概述
虚拟机把Class文件加载到内存,经过校验、解析和初始化,最终转换成虚拟机可以使用的Java类型,这就是Java的虚拟机类加载机制。
总体流程如图
一、加载
触发加载的时机
如果类没有初始化,那么以下几种方式会触发类的加载
- 当调用以下字节码指令
- 使用new初始化对象
- 访问类的static变量、static方法
- 反射访问类
- 触发了子类的初始化,先从父类开始初始化
- 虚拟机启动时需要执行的主类,也就是包含main函数的类
- 使用MethodHandler
这里有两个注意的点
- 通过数组定义来使用类,不会触发类的加载
- 尽量把固定的int值定位staic final常量,也不会触发类的加载(这是因为在编译阶段,对static final常量进行了优化,都放在常量池中,所以实际上触发的是对常量池的调用)
SubClass[] classArray = new SubClass[10];
public static final String value = "SubClass value";
还有一点,子接口的初始化不会影响父接口,只有用到父接口,比如父接口中定义的常量,才会触发父接口的初始化
加载
通过类的全限定名获取这个类的二进制字节流,在内存中生成代表这个类的class对象
- 加载阶段可以用系统的类加载器,也可以用自定义的类加载器
- 数组不通过类加载创建,而是虚拟机直接创建,但是数组的元素类型最终还是要通过类加载器去加载
二、验证
验证class字节流是否合法。
因为class字节流不一定是java源码编译而来,所以虚拟机需要保证class合法的,不会对载入了错误的字节流导致崩溃
- 文件格式验证:文件头是否合法
- 元数据验证:是否有父类,是否父类是不允许被继承的
- 字节码验证:语义合法
- 符号引用验证:当要把符号引用转换为直接饮用的时候,通过类中的权限定名是否能找到对应的类
三、准备
为类变量准备内存。
设置static变量的初始值,但跟真正的初始化有区别,此处只是赋一个默认值
public static int value = 123;
比如这种,会赋0。但是如果是这种,就会直接赋值
public static final int value = 123;
四、解析
将符号引用转换为直接引用。
符号引用:一组用来描述目标的符号,只要使用时能定位到目标即可
直接引用:直接指向目标的指针或句柄
解析内容
- 类或接口的解析
- 类或接口的字段、方法解析
五、初始化
调用cinit方法,进行变量的初始化。
cinit方法是编译器收集的所有变量赋值操作和static代码块合并而成,顺序是按源文件中的顺序决定的
- 先执行父类的cinit,然后在执行子类的
- 这就意味着父类是static代码块优于子类执行
虚拟机会保证多线程环境下的cinit方法的执行,多个线程访问,只有一个线程可以调用cinit方法,这就是内部类实现单例的原理
类加载器
使用与卸载就不多说了,当虚拟机发现没有对该类型的引用的时候,会触发类的卸载
什么是类加载器?通过一个类的全限定名来获取这个类的二进制流,放到java虚拟机外部去实现,以便让应用程序自己决定如何去加载这个类
类与类加载器
比较两个类是否相等,只有判断两个类,只有判断在同一个类加载器下才有意义,即使是同一个class对象,用不同的类加载器加载,那么这个类 必不相等
双亲委派模型
启动类加载器:JAVA_HOME\lib类库加载到虚拟机内存中
扩展类加载器:JAVA_HOME\lib\ext 目录中
应用程序类加载器:系统类加载器,加载用户类路径上所制定的类库。默认类加载器
- 如果一个类加载器收到了加载类的请求,都是先交给父类加载,每一层都是如此
- 只有父加载器反馈无法加载,子类加载器才会尝试加载
这样的好处是,有优先级的层次关系,比如object,哪个类加载器都要加载,最终委派给顶层
破坏双亲委派模型
重写loadclass方法,特殊的类自己处理,其他的调用super方法
参考
《深入理解JAVA虚拟机》