JVM-类加载机制

一个类的完整生命周期如下:

1.类加载过程

类加载机制:java虚拟机吧描述类的数据结构从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型,这个过程就是类加载机制。

1.1 加载

在加载阶段,虚拟机需要完成三个步骤:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。

    首先java虚拟机会按照CLASSPATH路径,加上类的全限定名来寻找是否有一个这样的xxx文件,比如有一个类的全限定名为jmv/demo1,通过demo1.class.getClassLoader().getResource("").getPath();可以获取到CLASSPATH为/D:/project/leetcode/out/production/leetcode/,所以这个文件的位置就是/D:/project/leetcode/out/production/leetcode/jmv/demo1.class,读取demo1.class,就可以获得二进制字节流。虚拟机并没有规定二进制字节流(Class文件)从哪里获取,以及如何获取,它可以从 ZIP 包中读取(日后出现的 JAREARWAR 格式的基础)、其他文件生成(典型应用就是 JSP)等等

    解释:class文件并不一定只能由Java源码编译而来,它可以使用包括靠键盘0 1直接在二进制编译器中敲出Class文件在内的任何途径产生。它也可以从各种包中生成等等

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    如果二进制字节流是从class文件中获取的话,会将其中的常量池映射到方法区的运行时常量池中,Code部分也会映射到方法区中。(类的字节码放入方法区,方法区中的数据存储格式完全由虚拟机实现自行定义,HotSpot内部采用 C++ 的 instanceKlass 描述 java 类)

  • 在内存中生成一个代表这个类的java.long.Class对象,作为方法区这个类的各种数据的访问入口。

    _java_mirror即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用 ,所有实例对象想要访问方法区中的字节码都需要去访问代表这个类的java.long.Class对象(类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息),这个对象将作为程序访问方法区中的类型数据的外部接口。

以上是非数组类的加载过程,对于数组类,它本身不通过类加载器创建,它是由java虚拟机直接在内存中动态构造的。

1.2 验证

连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身安全。

  1. 文件格式验证

    验证字节流是否符合Class文件格式的规范。

    • 是否以魔数0xCAFEBABE开头

    • 主版本号、次版本号是否在当前java虚拟机接收的范围之内

    • 常量池的常量中是否有不被支持的常量类型,tag

    • 指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量

    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据

    • 等等……..

  2. 元数据验证

    对字节码描述的信息进行语义分析,保证不存在与《java语言规范》定义相悖的元数据信息

    • 这个类是否有父类(除了java.lang.Object,所有的类都应该有父类)
    • 这个类是否继承了不允许被继承的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
    • 等等….
  3. 字节码验证

    对类的方法体(Class文件的Code属性)进行校验分析

    • 保证类型转换总是有效的,比如不能把父类对象赋值给子类数据类型,或者赋值给毫无关系的数据类型,只能是把子类对象赋值给父类数据类型。
    • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
    • 等等….
  4. 符号引用验证

    验证该类是否缺少或者禁止访问它依赖的某些外部类、方法、字段等资源

    • 符号引用通过字符串描述的全限定名是否可以找到这个类
    • 符号引用中的类、字段、方法是否可以被当前类访问,看访问修饰符
    • 等等….

1.3 准备

类中定义的变量(静态变量)分配内存并设置类变量初始值的阶段。

通常情况下,初始值是零值。

基本数据类型的零值:

特殊情况:当类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。

ConstantValue属性;使用位置:字段表;含义:由final关键字定义的常量值;

1.4 解析

java虚拟机将常量池中的符号引用替换为直接引用的过程。

符号引用:使用一组符号来描述所引用的目标,在内存中仅仅是一个符号,utf-8字符串,虚拟机并不知道这个符号代表的内容的确切位置。

直接引用:可以直接指向目标的指针,或者是能间接定位到目标的句柄,通过它虚拟机可以知道这个内容的内存地址,可以找到它。

1.5 初始化

直到初始化阶段,java虚拟机才真正开始执行类中编写的java程序代码,将主导权移交给应用程序。

在准备阶段,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

初始化阶段就是执行类构造器()方法的过程,()方法是javac编译器自动生成的。

编译期间编译器会由语句在源文件中出现的顺序依次收集类中所有类变量的赋值动作和静态语句块(static{ } )中的语句,并将它们合并在一起,放入自动生成的类构造器()中。

要点

  • java虚拟机中,父类的()方法先执行,然后再执行子类的()方法。
  • 如果类中没有类变量的赋值,静态语句块,编译器可以不为这个类生成()方法
  • 执行接口的()方法不需要先执行父接口的()方法,接口的实现类也是如此
  • 一个类的()方法在多线程环境中需要被加锁同步

2. 类加载器

通过一个类的全限定名来获取定义此类的二进制字节流。实现这一动作的代码被称为:类加载器

以JDK 8为例

名称 负责加载的类的位置 说明
Bootstrap Class Loader(启动类加载器) JAVA_HOME/jre/lib或者是被-Xbootclasspath参数所指定的路径 无法直接访问
Extension Class Loader(拓展类加载器) JAVA_HOME/jre/lib/ext 上级为Bootstrap,显示为null
Application Class Loader(应用程序类加载器) classpath 上级为Extension
自定义类加载器 自定义 上级为Application

2.1 Bootstrap ClassLoader(启动类加载器)

可通过在控制台输入指令,使得类被启动类加器加载。

因为启动类加载器是用c++语言编写的,所以启动类加载器无法被java程序直接引用,null就代表了启动类加载器。

负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。

2.2 Extension Class Loader(拓展类加载器)

这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以java代码的形式实现的。主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。

2.3 Application Class Loader(应用程序类加载器)

这个类加载器是在类sun.misc.Launcher$AppClassLoader中以java代码实现的,负责加载当前应用 classpath 下的所有 jar 包和类。

2.4 双亲委派模型

各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,这里的子父类不是继承的关系,是使用组合关系来复用父加载器的代码。

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则,进行加载,但是它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的类加载器都是这样,因此所有的加载请求最终都应该传送到最顶层的启动类加载器,只有当父加载器无法完成这个加载请求时,即它的搜索范围中没有找到所需的类,子加载器才会去尝试自己完成加载。否则就是父类去加载。

双亲委派模型的好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。比如java.lang.Object类,它存在于rt.jar包下,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们也编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

双亲委派源码:

private final ClassLoader parent;
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) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private final ClassLoader parent;
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) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}

if (c == null) {
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;
}
}

JDK9引入了java模块化系统,扩展类加载器被平台类加载器替代


JVM-类加载机制
https://vickkkyz.fun/2022/03/24/Java/JVM/4.类文件/第7章/
作者
Vickkkyz
发布于
2022年3月24日
许可协议