这篇文章我们关注一个问题:Java程序是怎么进入JVM并执行的?经常写Java程序的小伙伴应该都听说过类加载机制,在《深入理解Java虚拟机》里周老师已经讲的很清楚了,这篇随笔把之前的笔记以及一些总结重新梳理一下。前面我们已经知道 .java文件经过编译后变成Class文件,JVM加载的是字节码文件。这其中的细节不知道小伙伴们有么有了解过?
类从被JVM加载到内存开始,到卸载出内存为止,整个生命周期包括7个阶段,加载、验证、准备、初始化、卸载这个5个步骤顺序是确定的。
- 加载,类的加载时机由JVM自行决定,JVM需要完成3件事情:
- 通过类的全限定名获取类的二进制字节流
- 将类的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成类的Class对象,作为方法区数据的入口
数组在Java中是一种引用类型,数组类的加载由Java虚拟机直接创建。类加载完成后,二进制字节流存储在方法区中,HotSpot虚拟机会创建一个java.lang.Class对象作为程序访问方法区中的数据的外部接口,这个Class对象存储在方法区中,加载阶段何连接部分阶段是交叉进行的。
- 验证是连接部分的第一步,验证阶段是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。因为只有在字节码层面才能保证Java语言的相对安全性。验证阶段会大致完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的JVM处理。只有验证通过了,字节流才会进入方法区存储。
- 元数据验证:比如类是否有父类,类是否继承了不能被继承的类等,保证不存在不符合Java语言规范的元数据信息。
- 字节码验证:对类的方法体进行校验分析
- 符号引用验证:对常量池中的各种符号引用进行校验
- 准备:为类变量分匹内存并设置类型变量的零值阶段。类变量指的是被static修饰的变量,不包括实例变量。例如变量i,在准备阶段值就是0L,只有在执行方法的时候值才会变成1。
private static long a = 1;
- 解析:JVM将常量池内的符号引用替换为直接引用的过程。这里要解释下符号引用何直接引用。
- 符号引用: 用一组符号描述所引用的目标,引用的目标不一定已经加载到内存中。
- 直接引用:直接指向目标的指针、相对偏移量、间接定位到目标的句柄,直接引用的目标已经在内存中。
- 初始化,类加载过程的最后一步,执行类构造器<clinit>()方也就是真正执行类中定义的Java程序代码。遇到使用new关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法这些场景时,如果类没有被初始化,使用java.lang.reflect包的方法对类进行 反射调用时,那么一定要先初始化。还有一种场景是初始化子类时,如果父类没有被初始化,那么要先初始化父类。再看下接口的初始化,与类不同的地方就在于父类的初始化,子接口在使用到父接口的时候才初始化。在多线程环境中,如果多个线程同时初始化一个类,那么只有线程执行类的<clinit>()方法。
JVM加载字节码文件靠的是类加载器,这个操作是在JVM外部实现的。这样应用程序就可以自己决定如何获取所需的类。如果两个类来自同一个Class文件,但是由不同的类加载器加载,那么者两个类一定是不相等。从JVM角度讲,只有两种加载器。一种是启动类加载器,是虚拟机自身的一部分,由C++语言实现;还有就是其他类加载器,由Java语言实现,全都继承自抽象类java.lang.ClassLoader 独立于虚拟机外部。
从开发角度看,主要分为这三种:
- 启动类加载器(Bootstrap ClassLoader):加载<JAVA_HOME>/jre/lib目录中,或者被 -Xbootclasspath参数所指定的路径中。
- 扩展类加载器(Extension ClassLoader):主要加载<JAVA_HOME>/jre/lib目录中的
- 应用程序类加载器(Application ClassLoader):加载用户类路径(ClassPath)上所指定的类库。如果我们没有自定义过自己的类加载器,那么这就是程序默认的类加载器。
这些类加载器之间的关系为:
在使用类加载器加载类的过程种,最好遵循双亲委派模型。双亲委派的原理是:类加载器收到加载类的请求时,先把这个请求委派给父类加载去完成,每一层次的加载器按这个这个逻辑执行。那么所有的加载请求最终都应该传送到顶层的启动类加载中。父加载器无法加载,子加载器才会自己加载。这样做的好处是可以避免类的重复加载,保证程序运行的稳定性。
我们可以自定义类加载器,总结起来就是:(1)类继承ClassLoader (2) 重写findClass() 方法 (3) 调用defineClass()方法。在loadClass()里如果父类加载失败,调用findClass()方法加载,这样还是符合双亲委派机制的。破坏会双亲委派需要重写loadClass()方法。
参考资料:《深入理解Java虚拟机》第二版 周志明
《深入拆解Java虚拟机》郑雨迪