JVM虚拟机类加载机制(一)
类从被加载到虚拟机内存中开始,到卸载出内存截止,整个生命周期包括:加载、验证、准备、解析,初始化、使用、卸载七个阶段。其中验证、准备、解析三个部分统称为连接。
类初始化情况:
-
遇到new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果没有初始化,则需要触发初始化。生成这4条指令的最常见Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段,以及调用一个类的静态方法时。
-
使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要制定一个要执行的主类,虚拟机会先初始化这个主类。
以上四种场景成为类的主动引用。除此之外所有引用类的方式都不会触发初始化——被动引用。
第一种情况
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
package main.java.loadclass; public class SubClass extends SupperClass{ static { System.out.println("SubClass init!"); } }
package main.java.loadclass; public class SupperClass { static { System.out.println("supper class"); } public static int value = 123; }
package main.java.loadclass; public class NotInitialization { public static void main(String[] args){ System.out.println(SubClass.value); } }
输出: supper class 123
第二种情况
package main.java.loadclass; public class NotInitialization { public static void main(String[] args){ SupperClass[] sca = new SupperClass[10]; } }
运行此段代码无输出值,可见SupperClass未初始化。但是虚拟机会自动生成一个同名的类(继承自Object)进行初始化,创建动作由new array触发。
第三种情况
package main.java.loadclass; public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } package main.java.loadclass; /** * 常量在编译阶段会存入调用类的常量池中, * 本质上没有直接引入定义的常量类, * 因此不会触发定义常量类的初始化。 */ public class NotInitialization { public static void main(String[] args){ System.out.println(ConstClass.HELLOWORLD); } }
加载
加载过程
-
通过一个类的全限定名来获取定义此类的二进制字节流。
-
将这个字节流所带变的静态存储结构转化为方法区的运行时数据结构。
-
在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个数据的访问入口。
加载访问方式
-
从本地系统直接加载
-
通过网络下载class文件
-
从zip、jar等归档文件中加载class文件
-
从原有数据库中提取class文件
-
将java源文件动态编译为class文件
验证
目的
符合JVM加载字节码符合规范
验证过程
-
文件格式验证
-
元数据验证:是否符合java语言规范
-
字节码验证:确保程序语言合法,符合逻辑
-
符号引用验证:确保下一步的解析能正常执行。
准备
准备阶段的目的是为被加载的静态变量分配内存,并设置默认初始值,如果是final修饰的常量,初始值为设置的值。
解析
-
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
-
对类的字段和方法进行转换
-
符号引用:代码在编译阶段,虚拟机并不知道具体引用的地址,所以会用符号引用替代
-
直接引用:在内存中,将符号引用变成具体的引用地址,可以理解成一个指针,偏移量或者句柄
初始化
什么时候初始化?
-
当虚拟机启动时,初始化用户指定的主类。
-
使用new该类实例化对象的时候。
-
读取或设置静态字段的时候,初始化该静态字段的所在的类
-
调用类静态方法的时候,初始化该静态方法所在的类
-
使用反射class.forName("xxx")对类进行反射调用的时候,该类需要初始化。
-
子类的初始化会触发父类的初始化(1、接口除外,父接口在调用的时候才会初始化,2子类引用父类静态字段,只会引发父类的初始化),
-
如果一个接口定义了default方法,那么直接实现或间接实现接口的类的初始化,会触发该接口的初始化
-
当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类。
初始化顺序
初始化超类 》执行静态初始化 》类变量初始化
-
父类的静态变量和静态块赋值
-
自身的静态变量和静态块赋值
-
父类的成员变量和块赋值
-
父类构造器赋值:如果父类中含有有残构造器,则在子类构造器中一定要使用“super(参数)”指定调用父类有参的构造器,不然会报错
-
自身成员变量和块赋值
-
自身构造赋值
如何追踪类的加载与卸载:
-
追踪类的加载与卸载过程
-verbose:class
-
单独追踪类的加载
-XX:+TraceClassLoading
-
单独追踪类的卸载
-XX:+TraceClassUnloading