《Java 虚拟机》 类加载阶段

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 《Java 虚拟机》 类加载阶段

🟧1. 类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:、


加载(Loading)

验证(Verification)

准备(Preparation)

解析(Resolution)

初始化(Initialization)

使用(Using)

卸载(Unloading)

2e718f6de6d94987ae2f2182c528d9c8.png

其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,加载过程必须按这种顺序进行。由于 Java 语言支持运行时绑定(动态绑定),在某些情况下,解析阶段可以在初始化之后再开始,这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行的过程中调用、激活另一个阶段。

🟧2. 类加载的过程

🟠2.1 加载

在加载阶段,虚拟机需要完成的 3 件事情:


通过一个类的全限定名来获取定义此类的二进制字节流。二进制字节流获取的来源并没有严格指明,可以是 Class 文件、ZIP 包(JAR、EAR、WAR 格式的基础)、网络中获取、运行时计算生成、其他文件(如 JSP 文件)等等。

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

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

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:


_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用

_super 即父类

_fields 即成员变量

_methods 即方法

_constants 即常量池

_class_loader 即类加载器

_vtable 虚方法表

_itable 接口方法表

值得注意的的是,加载阶段和链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始。


注意:


instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),而元空间又位于本地内存中,但 _java_mirror 是存储在堆中

InstanceKlass 和 *.class (Java 镜像类)互相保存了对方的地址

类的对象在对象头中保存了 *.class 的地址,让对象可以通过其找到方法区中的 instanceKlass,从而获取类的各种信息2e718f6de6d94987ae2f2182c528d9c8.png

🟠2.2 链接

🔶2.2.1 验证

验证阶段是为了确保 Class 文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致有 4 个阶段的检验动作:


文件格式验证:保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个 Java 类型信息的要求。

元数据验证:对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,目的是确保解析动作能够正常执行,如果无法通过符号引用验证,那么将会抛出一个 java.lang.IncompatibleClassChangError 异常的子类。

🔶2.2.2 准备

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


static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾

static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成

如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成

如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

🔶2.2.3 解析

解析是 JVM 将常量池中的符号引用解析为直接引用的过程。


符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标内存即可。


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

🟠2.3 初始化

到了初始化阶段,才真正开始执行类中定义的 Java 程序代码,初始化阶段是执行类构造器 clinit() V 方法的过程,虚拟机会保证这个类的『构造方法』的线程安全。


clinit() V 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。


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

public class Test {
    static {
        i = 0;    // 给变量赋值可以正常编译通过
        System.out.println(i); // 编译器报错,非法向前引用
    }
    static int i = 1;
}

类的初始化的懒惰的,以下情况会初始化:


main 方法所在的类,总会被首先初始化

首次访问这个类的静态变量或静态方法时

子类初始化时,如果父类还没初始化,会先触发父类的初始化

使用 java.lang.reflect 包的方法对类进行反射调用时

使用 new 关键字实例化对象时

不会导致类初始化的情况


访问类的 static final 静态常量(基本类型和字符串)不会触发初始化,因为在编译器已经放入常量池中

子类访问父类的静态字段,只会触发父类的初始化,子类不会初始化

类对象.class 不会触发初始化

创建该类的数组不会触发初始化,创建动作由字节码指令 newarray 触发

类加载器的 loadClass 方法

Class.forName 的参数 2 为 false 时

验证类是否被初始化,可以看改类的静态代码块是否被执行。

🟧3. 类加载器

类加载阶段中的 ”通过一个类的全限定名来获取描述此类的二进制字节流“ 的动作是放到 JVM 外部实现的,实现这个动作的代码模块称为 ”类加载器“。

以 JDK 8 为例,类加载器分为以下几类:2e718f6de6d94987ae2f2182c528d9c8.png

启动类加载器是由 C++ 语言实现,是虚拟机自身的一部分,可通过在控制台输入指令,使得类被启动类加器加载。


其他的类加载器包括 Extension ClassLoader、Application ClassLoader 和自定义类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外部,并且继承 java.lang.ClassLoader.

🟠3.1 启动类加载器

C/C++ 语言编写,负责将存放在 < JAVA_HOME>\jr\lib\rt.jar 目录中的并能够被虚拟机识别的类库加载到虚拟机内存中,无法被 Java 程序直接引用,没有父加载器。

🟠3.2 扩展类加载器

Java 语言编写,负责加载 < JAVA_HOME>\lib\ext 目录中,或者被 java.exr.dirs 系统变量锁指定的路径中的所有类库,开发者可以直接使用,派生于 ClassLoader.

🟠3.3 系统类加载器

Java 语言编写,负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,派生于 ClassLoader.

🟠3.4 自定义类加载器

使用场景


防止源码泄漏

扩展加载源

隔离加载类:这些类都希望予以隔离,不同应用的同类名都可以加载,不冲突,常见于 Tomcat 容器

修改类加载的方式:例如,在需要时,可以动态加载

步骤


继承 ClassLoader 父类

遵从双亲委派机制,重写 findClass 方法,不是重写 loadClass 方法,否则不会走双亲委派机制

读取类文件的字节码

调用父类的 defineClass 方法来加载类

使用者调用该类加载器的 loadClass 方法

🟧4. 双亲委派模型

2e718f6de6d94987ae2f2182c528d9c8.png

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。值得注意的是,类加载器之间的父子关系一般不会以继承的关系实现,而是使用组合关系复用父加载器的代码。

🟠4.1 双亲委派模型的工作过程

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行

如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器

如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制

优点:


Java 类随着它的加载器一起具备了带有优先级的层次关系。

保证类不会重复加载和 Java 程序稳定运作,保证了 Java 核心 API 不会被随意替换,造成混乱。

实现双亲委派模型的代码集中在 java.lang.ClassLoader 的 loadClass 方法中,以下是 loadClass 方法的源码:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,检查请求的类是否已经被加载过了
        Class<?> c = findLoadedClass(name);
        //如果没有被加载过
        if (c == null) {
            try {
                //看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //看是否被启动类加载器加载过
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                  //如果父类加载器抛出 ClassNotFoundException
                  //说明父类加载器无法完成加载请求
            }
            if (c == null) {
                // 在父类加载器无法加载的时候
                // 再调用本身的 findClass 方法进行类加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

🟠4.2 破坏双亲委派模型

双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即 JDK1.2 面世以前的“远古”时代,建议用户重写 loadClass() 方法

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的。如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模型。

双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等,也就是希望应用程序能够像计算机外设一样,外设升级或者替换了,不重启机器就能使用。


相关文章
|
11天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
22天前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
28 3
|
2月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
100 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
3月前
|
Java
Java常见JVM虚拟机指令(47个)
Java常见JVM虚拟机指令(47个)
59 3
Java常见JVM虚拟机指令(47个)
|
3月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
24 3
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
134 0
|
3月前
|
Java 数据安全/隐私保护 Windows
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
|
3月前
|
存储 Java API
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
|
3月前
|
设计模式 存储 安全
18 Java反射reflect(类加载+获取类对象+通用操作+设计模式+枚举+注解)
18 Java反射reflect(类加载+获取类对象+通用操作+设计模式+枚举+注解)
90 0
|
4月前
|
存储 前端开发 Java
(二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器
上篇《初识Java虚拟机》文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再交由执行引擎执行。本文中则会对Java虚拟机的类加载机制以及执行引擎进行全面分析。