Java JVM类加载机制原理剖析

前言

      Java类要加载到JVM中的,会经过一系列的加载过程,这个过程就是在类加载子系统中实现的,当我们用Java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM中,本文会对这个加载过程以及双亲委派机制做剖析。
在这里插入图片描述

一、什么是类加载

      类加载其实是在硬盘上查找通过io读入字节码文件(class文件)并加载到JVM方法区/元空间中作为一个元数据模板,可以根据这个模板实例化出N个一模一样的实例对象,一个类只会被加载一次,并且是第一次使用时动态加载的。

  • 加载.calss文件的方式:
    • 从本地系统直接加载
    • 通过网络下载.class
    • 从zip、jar、war等归档文件中加载.class

二、类加载子系统

在这里插入图片描述

  • 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。

  • 加载阶段只负责class文件的加载,至于它是否可以运行,则由字节码执行引擎决定。

  • 加载的类信息存放于一块称为方法区/元空间的内存空间。除了类的信息外,方法区/元空间中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射),不同版本JDK也有各自的区别。

三、类的加载过程

      当JVM需要用到某个类时,虚拟机会加载它的.class文件,加载了相关的字节码信息后,会为它创建对应的Class对象,而这个过程就被称为类加载。但需额外注意的是:类加载机制只负责class文件的加载,至于是否可以执行,则是由执行引擎决定。
      类的生命周期包括 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载这7个阶段,其中类加载过程为加载 >> 验证 >> 准备 >> 解析 >> 初始化
在这里插入图片描述

2.1、加载

加载阶段是由类加载器进行的,加载.class文件的方式有:

从本地系统中直接加载
通过网络获取
从zip压缩包中读取,比如:jar、war格式的文件
运行时计算生成,如:动态代理技术

  • 加载过程完成以下三件事:
    • 1、通过类的完全限定名称获取定义该类的二进制字节流。
    • 2、将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
    • 3、在内存中生成一个代表该类的 Class 对象,作为方法区/元空间中该类各种数据的访问入口。

2.2、验证

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

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

2.3、准备

  • JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等),此时不会分配实例变量的内存,因为实例变量是在实例化对象时一起创建在Java 堆中的。从概念上讲,类变量所使用的内存都应当在 方法区/元空间 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 class 对象一起存放在 Java 堆中。
  • 这里所设置的初始值"通常情况"下是数据类型默认的零值(如 0、0L、null、false 等),比如我们定义了public static int i=996 ,那么 i 变量在准备阶段的初始值就是 0 而不是 996(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public final static int i=996 ,那么准备阶段 i 的值就被赋值为 996。

2.4、解析

  • 该阶段将常量池中的符号引用转化为直接引用。
  • 在 class 文件中常量池里面存放了字面量和符号引用,符号引用包括类和接口的全限定名以及字段和方法的名称与描述符。
  • 在 JVM 动态链接的时候需要根据这些符号引用来转换为直接引用存放内存使用。

2.5、初始化

  • 初始化阶段就是执行类构造器方法()的过程;
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来;
  • 构造器方法中指令按语句在源文件中出现的顺序执行;
  • 初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
    • 声明类变量是指定初始值
    • 使用静态代码块为类变量指定初始值
public class ClassInitDemo {
    
    
    private static int a = 1;

    // 静态代码块 在类初始化时调用
    static {
    
    
        System.out.println(a); //1
        a = 10;
        b = 20;
        System.out.println(a); //10
        //System.out.println(number);    //报错:非法的前向引用(可以赋值,但不能调用)
    }

    //链接阶段的准备环节:b = 0 --> 初始化阶段:2 --> 20
    private static int b = 2;

    // 代码块 会在实例前调用
    {
    
    
        System.out.println("代码块");
    }

    public ClassInitDemo(){
    
    
        System.out.println("ClassInitDemo");
    }

    public static void main(String[] args) {
    
    
        System.out.println(ClassInitDemo.a); // 10
        System.out.println(ClassInitDemo.b); // 20
        ClassInitDemo classInitDemo = new ClassInitDemo();
    }
}

类初始化时机

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 使用 java.lang.reflect 包的方法对类进行反射调用时,(如Class.forName(“com.kerwin.Test”)、newInstance())
  • 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
  • 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  • 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

四、类加载器(ClassLoader)

在这里插入图片描述

  • 引导/启动类加载器: Bootstrap ClassLoader,负责加载存放在jdk\jre\lib(jdk代表jdk的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类,比如java.lang.Integer均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

    注意:因为JVM是通过全限定名加载类库的,所以,如果你的文件名不被虚拟机识别,就算你把jar包丢入到lib目录下,引导类加载器也并不会加载它。出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类文件。

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

    这个类加载器是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$ExtClassLoader位置。它主要负责加载<JAVA_HOME>\lib\ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。它可以直接被开发者使用。

  • 系统/应用程序类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

    一般情况下,该类加载器是程序的默认类加载器,我们可以通过ClassLoader.getSystemClassLoader()方法可以直接获取到它。

  • 自定义类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

猜你喜欢

转载自blog.csdn.net/weixin_44606481/article/details/134838287