【JVM系列】java类加载机制详解

文章目录

  • 一、类的生命周期
  • 二、类的加载过程
    • 加载
    • 验证
    • 准备
    • 解析
    • 初始化
  • 三、类加载时机
  • 四、类加载器分类
  • 五、双亲委派原则
  • 六、Java字节码文件中的JVM指令


类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。

一、类的生命周期

【JVM系列】java类加载机制详解_第1张图片
包括以下 7 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading)

二、类的加载过程

包含了加载、验证、准备、解析和初始化这 5 个阶段。

加载

加载过程完成以下三件事:

  • 通过类的完全限定名称获取定义该类的二进制字节流。
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  • 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

验证

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

准备

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

此时不会分配实例变量的内存,因为实例变量是在实例化对象时一起创建在Java 堆中的。而且此时类变量是赋值为零值,即 int 类型的零值为 0,引用类型零值为 null,而不是代码中显示赋值的数值。

解析

该阶段将常量池中的符号引用转化为直接引用。

在 class 文件中常量池里面存放了字面量和符号引用,符号引用包括类和接口的全限定名以及字段和方法的名称与描述符。

在 JVM 动态链接的时候需要根据这些符号引用来转换为直接引用存放内存使用。

初始化

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

三、类加载时机

  • new、getstatic、putstatic、invokestatic 这 4 个字节码指令时对类进行初始化(即:实例化对象、读写静态对象、调用静态方法时,进行类的初始化);
  • 使用反射机制对类进行调用时,进行类的初始化;
  • 初始化一个类,其父类没有初始化时,先初始化其父类;
  • 虚拟机启动时,初始化一个执行主类;
  • 使用 JDK1.7 的动态语言支持时,如果 MethodHandle 实例的解析结果为 REF_getstatic、REF_putstatic、REF_invokestatic 的方法句柄(即:读写静态对象或者调用静态方法),则初始化该句柄对应类。

四、类加载器分类

讲到类加载不得不讲到类加载的顺序和类加载器。

Java 中大概有四种类加载器,分别是:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),系统类加载器(System ClassLoader),自定义类加载器(Custom ClassLoader),依次属于继承关系(注意这里的继承不是 Java 类里面的 extends)

【JVM系列】java类加载机制详解_第2张图片

  1. 启动类加载器(Bootstrap ClassLoader):主要负责加载存放在Java_Home/jre/lib下,或被-Xbootclasspath参数指定的路径下的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。
  2. 扩展类加载器(Extension ClassLoader):主要负责加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载Java_Home/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  3. 系统类加载器(System ClassLoader):主要负责加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  4. 自定义类加载器(Custom ClassLoader:自己开发的类加载器。

五、双亲委派原则

类加载器在加载 class 文件的时候,遵从双亲委派原则,意思是加载依次由父加载器先执行加载动作,只有当父加载器没有加载到 class 文件时才由子类加载器进行加载。这种机制很好的保证了 Java API 的安全性,使得 JDK 的代码不会被篡改。

六、Java字节码文件中的JVM指令

1、创建一个 Java 源文件 Test02.java,并在 main 方法中完成简单的逻辑操作,如下所示。

public class Test02 {
    public static void main(String[] args) {
        int i = 5;
        int j = 10;
        int k = i + j;
        System.out.println(k);
    }
}

2、在终端通过 javac 命令编译 HelloWorld.java。

javac Test02.java

3、反编译成我们能看懂的 JVM 指令,这里我们使用 javap -c 命令完成。

javap -c Test02.class

4、反编译之后的 JVM 指令如下所示。

1    Compiled from "Test02.java"
2    public class org.example.jvm.Test02 {
3     public org.example.jvm.Test02();
4        Code:
5           0: aload_0
6           1: invokespecial #1                  // Method java/lang/Object."":()V
7           4: return
8
9      public static void main(java.lang.String[]);
10        Code:
11           0: iconst_5
12           1: istore_1
13           2: bipush        10
14           4: istore_2
15           5: iload_1
16           6: iload_2
17           7: iadd
18           8: istore_3
19           9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
20          12: iload_3
21          13: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
22          16: return
    }

解释一下上述的 JVM 指令:

第 1 行表示当前的字节码文件编译自 Test02.java

第 3 行表示调用 Test02的无参构造函数来实例化当前对象。

第 4 行到第 7 行表示无参构造函数的执行流程。

第 5 行表示把 this 压入操作数栈中。

第 6 行表示调用 Test02父类 Object 的无参构造,我们知道每个对象在实例化的时候都会默认先实例化其父类对象,并且默认调用父类的无参构造。

第 7 行 return 表示构造方法执行完毕。

第 10 行到第 22 行表示 main 方法的执行流程。

第 11 行表示将常量 5 压入操作数栈。

第 12 行表示取出操作数栈栈顶元素,即 5,然后保存到局部变量表第 1 个位置,即变量 i。

第 13 行表示将常量 10 压入操作数栈。

第 14 行表示取出操作数栈栈顶元素,即 10,然后保存到局部变量表第 2 个位置,即变量 j。

第 15 行表示将局部变量表第 1 个变量(i)压入操作数栈。

第 16 行表示将局部变量表第 2 个变量(j)压入操作数栈。

第 17 行表示取出操作数栈中的前两个值相加,并将结果压入操作数栈顶。

第 18 行表示取出操作数栈栈顶元素,保存到局部变量表第 3 个位置,即变量 k。

第 19 行表示读取静态实例 PrintStream。

第 20 行表示将局部变量表第 3 个变量(k)压入操作数栈。

第 21 行表示调用 PrintStream 的 println 方法,将操作数栈顶元素(变量 k)输出。

第 22 行 return 表示 main 方法执行完毕。

你可能感兴趣的:(java虚拟机,java,jvm,后端)