Jvm类的加载机制

时间:2023-06-23 10:27:13

1.概述

虚拟机加载Class文件(二进制字节流)到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这一系列过程就是类的加载机制。

2.类的加载时机

类从被虚拟机加载到内存开始,直到卸载出内存为止,整个生命周期包括:加载——验证——准备——解析——初始化——使用——卸载 这7个阶段。其中验证、准备、解析3个部分统称为连接。

生命周期图如下:

Jvm类的加载机制

  其中加载、验证、准备、初始化、卸载这5个阶段顺序是确定的,类的加载过程必须按照这种顺序进行开始,而解析阶段则不一定:它在某种情况下可以在初始化之后再开始,这也是为了支持Java语言的动态绑定。

  哪些情况能触发类的初始化阶段?(前提:加载、验证、准备自然是已经执行完了)

  1. 遇到new、getstatic、putstatic、invokestatic 这4条指令时如果类没有初始化则会触发其初始化,(工作中触发这4种指令最常见的场景:new实例化对象、读取or设置类的静态字段【final修饰或者已经把静态字段放入常量池的除外】、调用类的静态方法)
  2. 使用反射的时候
  3. 初始化类的时候如果其父类还没进行初始化,则需要先触发父类的初始化
  4. 虚拟机启动时,需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
  5. 使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。切这个句柄对应的类没有初始化,则需要先触发其初始化

  注意:所有引用类的方式都不会触发初始化(被动引用)例如:创建数组、引用final修饰的变量、子类引用父类的静态变量 不会触发子类初始化但是会触发父类初始化

3.类的加载过程

- 加载

加载是类加载的一个阶段,在加载阶段  虚拟机需要完成下面3件事情

  1. 通过类的全限定名来获取定义此类的二进制字节流 
  2. 将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构
  3. 在内存中生成一个代表此类的java.lang.Object对象,作为方法区这个类的各种数据的访问入口

相对于类加载的其他阶段,加载阶段(准确的说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的。因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由开发人员自定义的类加载器来完成(即重写类加载器的loadClass()方法)。

加载完成后,外部的二进制字节流就转化成虚拟机所需的格式存储在方法区中,然后在内存中实例化一个java.lang.Class类的对象。这个对象将作为程序访问方法区中的这些类型数据的外部接口。

加载阶段与连接阶段的部分内容是交叉进行的,并不是加载完成后才能执行验证等操作。这些夹在加载之中的动作仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

- 验证

验证是连接的第一步,为了保证加载的二进制字节流所包含的信息是符合虚拟机规范的。

验证阶段大致分为下面4个检验动作:

  文件格式验证:验证字节流是否符合Class文件格式规范。例如:是否以魔数 0xCAFEBABE 开头、主次版本号是否在当前虚拟机处理范围内、常量池中的常量是否有不被支持的类型······。

  元数据验证:对字节码描述的信息进行语义分析。例如: 这个类是否有父类、是否正确的继承了父类。

  字节码验证:通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的(说白了就是对类的方法体进行分析确保方法在运行时不会危害虚拟机)。

  符号引用验证:确保解析动作能正常执行。

验证阶段是非常重要,但不一定是必要的阶段(因为对程序运行期没有影响)。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以使用-Xverify:none参数来关闭验证。

- 准备

正式为类变量分配内存并设置类变量初始值。这些变量所使用的内存都将在方法区中进行分配。

注意:

  • 此时被分配的仅仅是静态变量,而不是实例变量,实例变量将随着对象实例一起分配在Java堆中
  • 初始值通常情况下是数据类型的零值。假如定义一个静态变量 public static int value = 123;那么value在准备阶段初始值为0而不是123。
  • 被final修饰的变量在准备阶段就初始化为属性所指定的值。例如: public static final int value = 123;那么value在准备阶段初始值就是123。

- 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

符号引用:以一组符号来描述引用的目标,符号可以是任何形式的字面量。

直接引用:指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

- 初始化

初始化阶段是执行类构造器<clinit>()方法的过程。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员制定的参数值去初始化类变量和其他资源。

类构造器<clinit>()方法:是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。

编译器收集的顺序是由语句在源文件中出现的顺序决定的;静态代码块只能访问定义在静态块之前的变量,定义在它之后的变量,在前面的静态块中可以赋值,但不能访问。

非法向前引用示例

public class SuperClass {
public static int va;
static {
value = 1; //可以编译通过
va = value; //报错 非法向前引用
System.out.println("父类初始化");
} public static int value = 123;
}

<clinit>()方法 对类或接口来说并不是必须的,如果一个类中没有静态代码块,也没用对变量的赋值操作,那么编译器可以不为这个类生成<clinit>方法 

接口中不能使用静态块,但仍可以有变量赋值操作,因此接口和类一样都会生成<clinit>方法。不同的是,接口初始化不需要先执行父类的初始化,只有当父接口中的变量使用时,才会触发父接口的初始化。另外接口的实现类也不会触发接口的实例化。

虚拟机会保证一个类的<clinit>()方法在多线程中被正确的加锁、同步,如果多个线程去初始化一个类,那么只会有一个线程去执行类的<clinit>()方法,其他线程都处于等待状态。只能活动线程执行完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

4.类加载器

  虚拟机设计团队把类加载中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码块称为类加载器。

从Java开发人员的角度看,类加载器大致分为如下3种

  启动类加载器(Bootstrap Classloader):负责将存放在<JAVA_HOME>\lib(Javahome即jdk的安装目录)目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib下面也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接使用。

  扩展类加载器(Extension Classloader):该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的系统路径中的所有类库。开发者可以直接使用扩展类加载器。

  应用程序类加载器(Application Classloader):该加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用此加载器。如果应用程序中没有自定义的类加载器,那么这个就是程序默认执行的类加载器。(系统加载器)

我们的应用程序都是由这3种类加载器相互配合进行加载的。如果有必要,还可以加入自定义的类加载器。

这些类加载器之间的关系如下图:

Jvm类的加载机制

5.双亲委派模型:

  双亲委派模型的工作过程是:如果一个类加载器收到了一个类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的加载器都是如此,因此所有的加载请求最终都应该到达顶层的启动类加载器。只有当父加载无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派机制:

1、当ApplicationClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

ClassLoader源码分析:    

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查此类是否已被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//委派给父类加载器去加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果没有父加载器,则调用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果父加载器无法加载,则调用本身加载器去加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}  

双亲委派模型意义:

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

参考

《深入理解Java虚拟机》

https://www.cnblogs.com/ityouknow/p/5603287.html