深入理解java虚拟机——类加载机制(代码片段)

Kim_smile Kim_smile     2023-01-20     723

关键词:


类加载机制

类的生命周期

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)

类的加载过程

1、加载

类加载过程的第一步,Java虚拟机需要完成以下三件事情:

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

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证

3、准备

准备阶段是正式为类变量分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都应当在方法区中进行分配

  1. 这时候进行内存分配的仅包括类变量( 即静态变量,被 static 关键字修饰的变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

  2. 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中,这个时候类变量则会随着 Class 对象一起存放在 Java 堆中。

  3. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0.0、0L、null、false等),比如我们定义了public static int value = 123 ,那么 value 变量在准备阶段的初始值就是 0 而不是123(初始化阶段才会赋值)。

  4. 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化:比如给 value 变量加上了 final 关键字public static final int value = 123使其成为常量 ,那么准备阶段 value 的值就被赋值为 123。

4、解析

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

5、初始化

类的初始化阶段是类加载的最后一个步骤,此时 Java 虚拟机才真正开始执行类中编写的 Java 程序代码。

初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit> ()方法是编译之后自动生成的。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,即构造器方法中指令按语句在源文件中出现的顺序执行。
  • <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
  • 对于<clinit>() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit>() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。

类的初始化时机

对于初始化阶段,虚拟机严格规范了有且只有5种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 当遇到 newgetstaticputstaticinvokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forName("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载器

类与类加载器

比较两个类是否 “相等” ,首先需要这两个类来源于同一个Class文件,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

这里的 “相等” ,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。


类加载器分类

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
  • 其它所有的类加载器,使用 Java 实现,独立存在于虚拟机外部,全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader):此类加载器负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。(String等核心类库)
  • 扩展类加载器(Extension ClassLoader):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  • 应用程序类加载器(Application ClassLoader)由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

如果想要自定义加载器,需要继承 java.lang.ClassLoader类,且为了满足双亲委派机制,需要指定父加载器为拓展类加载器


双亲委派模型

下图展示了各种类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承关系(Inheritance)来实现的,而是通常使用组合关系(Composition)来复用父加载器的代码

工作过程

每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的加载请求最终都应该传送到最顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理这个加载请求时,子加载才会尝试自己去完成加载。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

当一个 .class 文件要被加载时,不考虑我们自定义类加载器类,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载;如果没有会交到父加载器,然后调用父加载器的loadClass方法。父加载器同样也会先检查自己是否已经加载过,如果没有再往上,直到到达BootstrapClassLoader之前,都是在检查是否加载过,并不会选择自己去加载。到了根加载器时,才会开始检查是否能够加载当前类,能加载就结束,使用当前的加载器;否则就通知子加载器进行加载;子加载器重复该步骤。如果到最底层还不能加载,就抛出异常ClassNotFoundException

总结:所有的加载请求都会传送到根加载器去加载,只有当父加载器无法加载时,子类加载器才会去加载


源码分析

核心方法为 java.lang.ClassLoader 的 loadClass()

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) 
                // 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
            
            if (c == null) 
                long t1 = System.nanoTime();
                // 在父类加载器无法加载时,再调用本身的findClass方法来进行类加载
                c = findClass(name);

                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 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改(沙箱安全机制)。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

  • 避免类的重复加载
  • 保证Java核心类库的安全

参考资料

《深入理解Java虚拟机 第3版》——周志明

https://github.com/Snailclimb/JavaGuide

深入理解java虚拟机--虚拟机类加载机制(代码片段)

...4.双亲委派机制4.1双亲委派机制说明4.2好处本文参考于《深入理解Java虚拟机》1.虚拟机类加载机制说明Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接... 查看详情

深入理解java虚拟机类加载机制

本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。本系列其他文章:【深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM【深入理解Java虚拟机】垃圾回收机制1、类加载机制概述虚拟机把... 查看详情

《深入理解java虚拟机》-----第7章虚拟机类加载机制——java高级开发必须懂的(代码片段)

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。7.1概述上一章我们了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能... 查看详情

深入理解jvm虚拟机读书笔记——类的加载机制(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化&#... 查看详情

深入理解jvm虚拟机读书笔记——类的加载机制(代码片段)

注:本文参考自周志明老师的著作《深入理解Java虚拟机(第3版)》,相关电子书可以关注WX公众号,回复001获取。Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化&#... 查看详情

深入理解jvm(③)虚拟机的类加载时机(代码片段)

前言Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。类加载的时机一个类型从被加载到虚拟机内存中... 查看详情

虚拟机类加载机制(代码片段)

本文参考《深入理解java虚拟机》虚拟机加载类的过程:java源程序被编译器编译成class文件后,会被虚拟机的类加载器加载,加载完成后,会为class文件在内存中开辟一块空间用来存储class文件对象,之后对class文件进行校验,准... 查看详情

深入理解java虚拟机——类加载机制概述

一、类加载机制概述虚拟机把描述类的数据从Class文件加载加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加... 查看详情

深入理解java虚拟机——类加载机制概述

一、类加载机制概述虚拟机把描述类的数据从Class文件加载加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加... 查看详情

虚拟机类加载机制(代码片段)

摘自《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版)       虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型ÿ... 查看详情

jvm|第2部分:虚拟机执行子系统《深入理解java虚拟机》(代码片段)

...节码执行引擎7.1确定被调用的方法最后前言参考资料:《深入理解Java虚拟机-JVM高级特性与最佳实践》第1部 查看详情

深入理解java虚拟机---4虚拟机类加载机制

类加载的整个生命周期:   加载、连接(验证、准备、解析)、初始化、使用、卸载。加载:    class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态数据结构转化为方法区中运行的数据结构,... 查看详情

深入理解jvm读书笔记三:虚拟机类加载机制

Java虚拟机类加载机制是把Class类文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。7.1概述与那些在编译时需要进行链接工作的语言不同,在Java语言里面,... 查看详情

深入理解jvm:类加载机制

概述虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。与那些在编译时需要进行链接工作的语言不同,在Java语言里... 查看详情

《深入理解jvm——虚拟机类加载机制》

JVM深入理解JVM(5)——虚拟机类加载机制 PostedbyCrowonAugust21,2017在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机中,而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟... 查看详情

java实现代码热更新(代码片段)

...一个简易的代码热更新工具。类加载相关知识可以参考:深入理解JVM虚拟机第三版,深入理解JVM虚拟机(第二版)—国外的,自己动手写JVM类加载器JVM通过Cl 查看详情

java实现代码热更新(代码片段)

...一个简易的代码热更新工具。类加载相关知识可以参考:深入理解JVM虚拟机第三版,深入理解JVM虚拟机(第二版)—国外的,自己动手写JVM类加载器JVM通过ClassLoader将.class二进制流读取到内存中, 查看详情

类加载机制你真的了解吗?(代码片段)

...ass文件中的描述信息加载到内存中运行和使用。以下是《深入理解Java虚拟机第二版》对类加载器机制的定义原文:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直... 查看详情