学习笔记 | JVM
一、深入理解JVM
1. 类加载器
- 在java中,类型的加载,连接与初始化过程都是在程序运行期间完成的
- 类的生命周期
- 加载:查找并加载类的二进制数据
- 连接:
- -验证:确保被加载的类的正确性
- -准备:为类的静态变量分配内存,并将其初始化为默认值
- -解析:把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值
- 使用
- 卸载
jvm的符号引用与直接引用:
在jvm中类的连接过程中,在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用
符号引用:
在编译时,Java类并不知道其所引用的类的实际地址,因此只能用符号引用来代替.比如说A类引用了B类,在编译时A类不知道B类的实际内存地址,因此只能用符号来表示B类的地址(比方说包名+类名)
直接引用:
直接指向目标的指针或偏移量或句柄
- Java程序对类的使用方式可分为两种
- 类或者接口的初始化时机
类的主动使用包括:
1. 创建类的实例
2. 读取或修改类的静态变量
3. 调用类的静态方法
4. 反射
5. 初始化这个类的某个子类
6. Java虚拟机启动时被标明为启动类的类
7. JDK1.7开始提供的动态语言支持
类的被动使用:
除去上面类的主动使用,其他的都是被动使用
主动使用和被动使用的区别:
只有当类的首次主动使用时才会去初始化这个类,
- 父类的所有子类都共享父类的静态变量,有且仅有一份,对其进行修改,会影响到所有的子类及对象
- 对于静态方法或静态字段的主动使用(调用静态方法,获取静态变量),只有直接定义了该方法或字段的类才会被初始化
public class Test {
public static void main(String[] args) {
System.out.print(B.str);
}
}
class A {
public static String str = "1";
static {
System.out.print("2");
}
}
class B extends A {
static {
System.out.print("3");
}
}
-XX:+TraceClassLoading
查看虚拟机类的加载
- JVM参数格式
-XX:+<option>
表示开启option选项
-XX:-<option>
表示关闭option选项
-XX:<option>=<value>
表示为option赋值为value
- 常量在编译阶段会存入到调用这个常量所在的类的常量池中.因此,读取某个类的常量不会导致该类初始化
public class Test {
public static void main(String[] args) {
System.out.println(A.str);
}
}
class A {
public static final String str = "1";
static {
System.out.print("2");
}
}
- 如果类的常量在编译阶段不能被确定,那么其值就不会被放入到调用类的常量池中,这时就会导致类的初始化
public class Test {
public static void main(String[] args) {
System.out.println(A.str);
}
}
class A {
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
A[] a = new A[10];
}
}
class A {
public static String str = "1";
static {
System.out.print("2");
}
}
public class Test1 {
public static void main(String[] args) {
A aa = C.aa;
}
}
class A {
public A(String str){
System.out.println(str);
}
}
interface B {
A a = new A("B");
}
interface C extends B{
A aa = new A("C");
}
- 类加载器的种类
- 概览
- Bootstrap ClassLoader
- 根加载器,加载JRElib
t.jar或者-Xbootclasspath选项指定的jar包
- Extension ClassLoader
- 加载JRElibext*.jar或者-Djava.ext.dirs指定目录下的jar包
- App ClassLoader
- 加载CLASSPATH或者-DJava.class.path所指定的目录下的类和jar包
- Custom ClassLoader
- 通过java.lang.ClassLoader的子类自定义加载class
- 调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化
- 调用Class类的forName方法,是通过反射获取类的信息,会导致类的初始化
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> clazz = loader.loadClass("A");
clazz = Class.forName("A");
}
}
class A {
static {
System.out.println("1");
}
}
- 获得ClassLoader的途径
- 获得当前类的ClassLoader
- 获得当前线程上下文的ClassLoader
- Thread.currentThread().getContextClassLoader()
- 获得系统的ClassLoader
- ClassLoader.getSystemClassLoader()
- 获得调用者的ClassLoader
- DriverManager.getCallerClassLoader()
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Test.class;
System.out.println(clazz.getClassLoader());
clazz = String.class;
System.out.println(clazz.getClassLoader());
}
}
- ClassLoader通过类的二进制名称来定位一个类
- 类的二进制名称
- 包名.类名$内部类$匿名内部类序号
- java.lang.String
- javax.swing.JSpinner$DefaultEditor JSpinner的DefaultEditor内部类
- java.security.KeyStore$Builder$FileBuilder$1 FileBuilder内部类中第一个匿名内部类
- 数组的类加载器
- 数组类的Class对象并不是由类加载器来加载的,而是由JVM在运行时根据需要动态生成的,数组类的父类是Object
- 对数组类调用getClassLoader方法,返回的是数组当中元素类的类加载器
- 数组中元素类型是原生类型,则这个数组是没有类加载器的
- Java中原生类型包括: byte,short,int,long,float,double,boolean,char
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Test[] b = new Test[1];
System.out.println(b.getClass().getClassLoader());
int[] c = new int[1];
System.out.println(c.getClass().getClassLoader());
}
}
- 自定义类加载器
- 自定义的类加载器必须继承ClassLoader类
- 可以指定类加载器的父类加载器,默认是AppClassLoader
- 重写findClass方法,用来自定义的类的加载方式
- 通过类名或url获取到要加载的类的.class文件的内容,存入到 byte数组的data中
- 通过defineClass方法,传入data信息,完成类的加载并返回类对象
- 调用loadClass方法来加载类,并返回要加载的类的对象
- loadClass方法首先会去当前类加载器的命名空间中查找已加载的所有类中是否有要加载的类,如果有直接返回该类对象
- 如果该类还未被加载,调用当前类加载器父类的loadClass方法,如果父类加载器是null,则调用BootstrapClassLoader的loadClass方法
- 如果循环调用了父类的loadClass后,要加载的对象还是null,则调用当前类加载器的findClass方法来加载类
public class Test {
public static void main(String[] args) throws Exception {
TestClassLoader loader = new TestClassLoader("/Users/serenityma/Desktop/");
Class<?> clazz = loader.loadClass("Test1");
Object object = clazz.newInstance();
System.out.println(object);
System.out.println(object.getClass().getClassLoader());
}
}
class TestClassLoader extends ClassLoader {
private static final String fileExtension = ".class";
private String classPath;
public TestClassLoader(String classpath) {
super();
this.classPath = classpath;
}
protected Class findClass(String className) {
try {
byte[] data = null;
String filePath = this.classPath + className + fileExtension;
FileInputStream fis = new FileInputStream(new File(filePath));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = -1;
while ((len = fis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
data = baos.toByteArray();
fis.close();
baos.close();
return this.defineClass(className, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
- 类加载器的命名空间
- 每个类都有自己的命名空间,命名空间由该加载器以及其父加载器所加载的所有的类组成
- 同一个命名空间中不会出现两个相同的类
- 不同的命名空间可能出现名称相同的类,比如说自定义的类加载器A和B,他们的父类加载器都是AppClassLoader,分别调用A和B的loadClass方法来加载同一个名称的Java类,这时就会有两个相同名称的Java类同时被加载了,他们的Class类却不是同一个.
public class Test {
public static void main(String[] args) throws Exception {
TestClassLoader loader1 = new TestClassLoader("/Users/serenityma/Desktop/");
TestClassLoader loader2 = new TestClassLoader("/Users/serenityma/Desktop/");
Class<?> clazz1 = loader1.loadClass("Test1");
Class<?> clazz2 = loader2.loadClass("Test1");
System.out.println(clazz1 == clazz2);
}
}
- 类加载器命名空间的包含关系
- 子加载器加载的类可以访问到父加载器加载的类,而父加载器加载的类不能访问子加载器加载的类.
- 类的卸载
- 由JVM自带的类加载器加载的类,在JVM的生命周期内,始终不会被卸载,JVM自带的类加载器包括BootstrapClassLoader,ExtClassLoader,AppClassLoader.
- 由用户自定义的类加载器所加载的类是可以被卸载的.
- 当类的Class对象不再被引用,Class对象的生命周期就结束了,它在方法区内的数据也会被卸载.
- 通过-XX:TraceClassUnloading参数可以查看类的卸载情况
- 通过jvisualvm参数可以可视化的查看java运行情况
public class Test {
public static void main(String[] args) throws Exception {
TestClassLoader loader1 = new TestClassLoader("/Users/serenityma/Desktop/");
Class<?> clazz1 = loader1.loadClass("Test1");
System.out.println(clazz1);
clazz1 = null;
loader1 = null;
System.gc();
Thread.sleep(100000);
}
}
- 类的预先加载
- JVM规范允许类加载器在预料到某个类将要被使用时预先加载它,如果在预先加载的过程中遇到了.class文件缺失或错误,类加载器必须在程序首次主动使用该类时才报错.
- 如果这个类一直没有被主动使用,那么类加载器就不会报错.
- 类加载器的双亲委派模型的好处
- 可以确保Java核心库的类型安全:借助双亲委派机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用所使用的都是同一版本的Java核心类库,它们之间是互相兼容的.
- 可以确保Java核心类库所提供的类不会被自定义的类所替代
- 不同的类加载器可以为相同名称的类创建额外的命名空间.相同名称的类可以并存在Java虚拟机中,只要用不同的类加载器来加载他们即可.这类技术在很多框架中都得到了实际应用.
- 双亲委派模型的问题所在及解决办法
- 每个类都都会使用自己类的类加载器去加载它所依赖的其他的还未被加载的类
- 在双亲委派模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载.但对于SPI(Service Provider Interface)来说,有些接口是Java核心库所提供的,而这些接口的实现却是来自不同的厂商,所以这些接口是由启动类加载器加载的,而启动类加载器不会去加载其他来源的jar包,这样传统的双亲委派模型就无法满足SPI的要求.
当使用Bootstrap加载器加载一个对象并使用时,如果该对象内部要使用在classpath下还未加载的一个类对象,但是根据双亲加载机制boostrap尝试加载该类,因为其自身为最高层加载器所以只能由boostrap加载器加载,所以是无法加载该类的,也就无法使用该类,这时候就应该打破双亲委派模型,使用线程上下文加载器来加载该类.
- 启动类加载器(BootstrapClassLoader)
- 启动类加载器(BootstrapClassLoader)并不是Java类,而是特定于平台的机器指令,它负责开启整个加载流程.
- 除了启动类加载器之外的所有类加载器都是Java类实现的,所以需要有一个组件来加载第一个Java类加载器,这个组件就是启动类加载器
- 启动类加载器还会负责加载JRE正常运行所需要的基本组件,包括java.util和java.lang包中的类等等
- ClassLoader中的getSystemClassLoader方法
- 概述: 获取系统类加载器,默认是AppClassLoader,可以通过
-Djava.system.class.loader=MyClassLoader
参数来指定自定义的类加载器作为系统类加载器
- ClassLoader类中定义了一个私有的静态变量scl,用来记录当前程序的系统类加载器对象,还有一个boolean型的私有静态变量sclSet,用来表示程序是否已经设置了系统类加载器.
- 调动getSystemClassLoader方法时,首先会去判断是否已经设置了系统类加载器
- 如果已经设置了系统类加载器,直接返回该系统类加载器
- 如果还没有设置,就将AppClassLoader设置为系统类加载器
- 如果程序运行时设置了参数
java.system.class.loader
来指定自定义的类加载器作为系统类加载器,那么就会通过反射方式实例化这个指定的类加载器,将这个类加载器作为系统类加载器,并把AppClassLoader设置为这个类加载器的父类加载器.
- Class类的forName方法解析
- forName方法有两个重载方法,一个只有一个参数,另一个有三个参数
- forName(String,boolean,ClassLoader),第一个参数是要加载的类或接口的二进制名称,第二个参数表示是否要初始化,第三个参数表示用指定的类加载器来加载,如果为null就用bootstrap classloader来加载
- forName(String) 默认要初始化,使用当前类的类加载器来加载.
- Launcher类解析
- AppClassLoader和ExtClassLoader都是Launcher类中的静态内部类,Launcher类位于sun.misc包下
- Launcher类在程序运行时就会实例化,然后在它的构造方法中就会将ExtClassLoader和AppClassLoader都实例化出来,并设置主线程的线程上下文加载器为AppClassLoader
- 线程上下文类加载器解析
- 每个线程都可以调用getContextClassLoader和setContextClassLoader方法来获取或设置上下文类加载器.
- 当创建一个新线程时,它的context classloader 和当前线程的context classloader一样.
- SPI(Service Provider Interface 服务提供者接口)
- Java核心库提供了一些功能的接口,但是具体实现是由外部加载的类来实现.
- ServiceLoader服务提供者加载类,通过服务接口类,加载服务具体的实现类.
- 内部成员变量
- ServiceLoader的构造方法全是私有的,只能通过静态方法load来返回ServiceLoader实例
- ServiceLoader.load方法,实例化一个指定服务的ServiceLoader实例
- 需要指定要加载的服务接口,例如
Drive.class
- 可以手动指定类加载器,如果不指定,默认是线程上下文类加载器
Thread.currentThread().getContextClassLoader()
- load方法会去查找classpath下所有jar包的META-INF/services目录里文件名是要加载的接口全限定名的文件,按行读取文件里面的实现类名并用指定的类加载器加载.
- JDBC连接过程
public class Test {
public static void main(String[] args) {
try {
Connection connection = DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:3306/test?useSSL=false&characterEncoding=UTF-8",
"root", "root");
Statement statement = connection.createStatement();
String sql = "update test.user set user_name = 'A' where user_id = 1";
statement.execute(sql);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- DriverManager.getConnection背后原理
- DriverManager有个非常重要的内部成员变量registeredDrivers,用来保存已经注册的Driver类.
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
- DriverManager有个静态代码块,用来注册所有的Driver实现类,注册过程如下:
-
- 从系统变量里取出设置的
jdbc.drivers
变量,使用Class.forName去加载设置的类.(也就是去加载系统启动时通过-Djdbc.drivers=XXX
设置的jdbc驱动类)
-
- 使用ServiceLoader.load方法获得Driver所有实现类的迭代器,因为这个迭代器是懒加载的,所以要遍历一下它,把所有的Driver实现类都加载到jvm中.
- 每个Driver实现类中又有一个静态代码块,调用DriverManager.registerDriver,把自己的实例放入到DriverManager的registeredDrivers中.
2. 字节码
- 使用javap -verbose命令可以查看class文件的字节码信息.
- 使用hex fiend软件可以直接查看.class文件的二进制内容.
- 字节码包括的信息有:
- 魔数,版本号,常量池,类信息,类的构造方法,类中的方法信息,类变量与成员变量等信息
- 魔数:所有的.class字节码文件的前4个字节都是魔数,魔数的值固定为:0xCAFEBABE,(记忆方法:cafe babe咖啡宝贝)
- 魔数之后的四个字节为版本信息,前两个字节表示次版本号(minor version),后两个字节表示主版本号(major version).比如版本号为 00 00 00 34,转换为十进制,前面00 00表示此版本号为0,后面00 34表示主版本号为52,主版本号52代表jdk1.8,所以该文件的版本号为1.8.0.可以通过java -version来验证.
- synchronized关键字在字节码中的体现
- 方法上申明synchronized,使用当前类作为锁,在字节码中可以看到方法的访问修饰符多了一个ACC_SYNCHRONIZED
- synchronized代码块在字节码中的体现
- synchronized修饰静态方法,是给当前类的Class对象上锁
- synchronized修饰普通方法,是给调用这个方法的实例上锁
- synchronized修饰代码块,使用提供的类加锁
- Java类中代码的执行过程,字节码中的
<clinit>
和<init>
方法
- 在字节码中,类的初始化方法被封装在
<clinit>
中,实例化方法被封装在<init>
中,clinit方法只有一个,init方法看构造方法的数量,至少有一个
- 在类的准备阶段,所有的变量已经被赋予过一个默认的初始值了,而在初始化阶段会执行类构造器
<clinit>
,这时会给静态变量赋予程序设定的初始值.
- 子类的clinit执行之前,父类的clinit已经执行完毕.
- 执行接口的clinit不需要先执行父接口的clinit,只有当父接口中定义的变量被引用时,才会去执行父接口的初始化.接口的实现类在初始化时也不需要执行接口的clinit
*实例方法中为什么可以使用this变量
- 在编译阶段,类的非静态方法会被自动添加this作为第一个参数,所以在字节码中看到的方法的args_size总是比实际代码中传入参数的个数多一个.
- 所以编译器在编译时将对this的访问转化为对一个普通实例方法参数的访问,在运行期间,JVM调用这个实例方法时,自动向实例方法传入该this参数.所以,在实例方法的局部变量表中,至少会有一个指向当前对象的局部变量.
- 字节码相关
- invokeinterface: 调用接口中的方法,在运行期决定到底调用该接口的哪个对象的特定方法
- invokestatic: 调用静态方法
- invokespacial: 调用自己的私有方法,构造方法或者父类的方法
- invokevirtual: 调用虚方法,运行期动态查找的过程
- invokedynamic: 动态调用方法
- 有些符号引用是在类加载阶段或是第一次使用时就会被转换为直接引用,这种转换叫做静态解析;另外一些符号引用则是在每次运行期转换为直接引用,这种转换叫做动态链接,这体现为Java的多态性
- 静态解析的4中情形:
- 静态方法
- 父类方法
- 构造方法
- 私有方法
- 以上4类方法称作非虚方法,他们是在类加载阶段就可以将符号引用转换为直接引用的
- 变量的
静态类型
和实际类型
- 静态类型是变量申明时指定的类型,实际类型是变量被赋值的类型.变量的静态类型是不会改变的,而变量的实际类型可以发生改变,这也是Java多态的一种体现.静态类型在编译时就已经确定,而实际类型在运行期才可确定.
- 方法的重载,就是根据变量的静态类型来确定具体执行哪个方法的.
- 比如
Animal a = new Cat();
a的静态类型就是Animal,实际类型就是Cat
public class Test {
public void run(A1 a){
System.out.println(1);
}
public void run(B1 b){
System.out.println(2);
}
public static void main(String[] args) {
A1 a = new A1();
A1 b = new B1();
Test test = new Test();
test.run(a);
test.run(b);
}
}
class A1 {}
class B1 extends A1{}
- 创建对象语句在字节码中的实现
- 在内存中开辟空间,创建类的实例(这时还没有调用init方法),并将内存地址压栈
- 复制当前栈顶的元素,并压栈(即复制一份当前创建的实例的地址)
- 弹出栈顶元素并调用它的init方法
- 弹出栈顶元素并赋值给局部变量表中某个元素
- 方法的重写和重载
- 方法的重载是静态的,是编译期行为,JVM在编译阶段就已经确定具体调用的是哪个方法;方法重写是动态的,是运行期行为,JVM在运行期间才能确定具体调用的那个方法.
3. 垃圾回收器
- JVM运行时内存数据区域
- 线程共享:
- 方法区 Method Area
- 堆 Heap
- 线程隔离:
- Java虚拟机栈 JVM Stack
- 本地方法栈 Native Method Stack
- 程序计数器 Program Counter Register
Java虚拟机栈:
Java虚拟机栈描述的是Java方法的执行模型:每个方法执行的时候都会创建一个栈帧(Stack Frame),栈帧用于存放局部变量表,操作栈,动态链接,方法出口等信息.一个方法的执行过程,就是这个方法对于栈帧的入栈出栈过程.
堆(Heap):
堆里存放的是对象的实例,堆是Java虚拟机管理内存中最大的一块,GC的主要的工作区域,为了高效的GC,会把堆细分更多的子区域.
方法区域:
存放每个Class的结构信息,包括常量池,字段描述,方法描述.它是GC的非主要工作区域
- JVM垃圾回收模型
- 垃圾判断算法
- 引用计数算法(Reference Counting)
- 给对象添加一个引用计数器,当有一个地方引用这个对象,计数器加1,当引用失效,计数器减1,计数器为0的对象就是不可能再被使用的对象,可以被垃圾回收器回收掉
- 引用计数算法无法解决对象循环引用的问题
- 根搜索算法(Root Tracing)
- 通过一系列的称为'GC Roots'的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的.
- GC Roots包括:
- 在JVM栈中的引用
- 方法区中的静态引用
- JNI(即Native方法)中的引用
- 方法区的垃圾回收,会回收哪些东西
- 方法区垃圾回收主要回收两个部分内容:废弃常量和无用类
- 什么是无用类,类的回收需要满足一下三个条件:
- JVM中不存在该Class的任何实例
- 加载该类的ClassLoader已经被GC
- 该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法.
- 在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证方法区不会溢出.
- 垃圾回收算法
- 复制算法
缺点:浪费空间
- 标记清除算法
缺点:产生内存碎片
- 标记整理算法
缺点:需要耗费时间来进行整理这个动作
- 垃圾回收器
Diagram