1.什么是类的加载
类的加载是指将编译完成的字节码文件即class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2.什么时候进行类加载
jvm可以实现预加载功能,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范规定JVM可以预测加载一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错。
3.类的生命周期
前五个阶段为类的加载过程,其中加载、验证、准备、初始化四个阶段开始顺序是确定的,而解析阶段可能发生在初始化之后,这是为了支持java动态绑定。而前五个阶段是按顺序开始的,通常这些阶段会交叉进行,通常在一个阶段执行过程中调用或激活另一个阶段。
3.1加载
JVM需要完成三件事情
- 类加载器通过类的全路径限定名读取类的二进制字节流
- 将二进制字节流代表的类结构转化到运行时数据区的方法区中
- 在堆中生成代表这个类的java.lang.Class实例,作为对方法区数据访问入口
3.2验证
加载和验证是交叉进行的,验证的主要目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段非常重要但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3.3准备
准备是为类静态变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
- 进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int i = 2;
i在准备阶段过后的初始值为0,而不是2,因为这时候尚未开始执行任何Java方法,而把value赋值为2的public static指令是在程序编译后,存放于类构造器()方法之中的,所以把i赋值为2的动作将在初始化阶段才会执行。
- 如果类字段的字段属性即同时被final和static修饰,那么在准备阶段变量value就会被初始化为属性所指定的值。
类变量i被定义为: public static final int i = 3;
编译时Javac将会为i生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。
3.4解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
3.5初始化
初始化类的静态变量和静态代码块为用户自定义的值,在类被Java程序“第一次主动使用”的时候,才会触发初始化操作。其中包括:
- new了一个类的对象
- main方法所在类
- 调用了类的静态成员和静态方法
- 修改类的静态成员的值
- 使用java.lang.reflect包对类进行反射调用
- 初始化子类时,父类未被调用,则先初始化父类
4.类加载器
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系如下:
继承关系可通过以下程序验证:1
2
3
4
5
6
7
8public class Main{
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
输出
sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3
null
注: ExtClassLoader的父类为null,是因为BootStrap ClassLoader使用C语言实现的。
Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。
Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。
App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。
User ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
结束生命周期
在如下几种情况下,Java虚拟机将结束生命周期
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止