tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

  • JVM合集

    • JVM的作用跟字节码
    • 类加载的过程
      • 类加载的几个阶段
        • 加载
        • 链接
        • 验证/校验
        • 准备
        • 解析
        • 初始化
      • 类加载器机制
        • 其他类加载器(用户自定义类加载器)
        • 双亲委派模型的工作流程
        • 类加载器的归属关系总结
        • 关键区别与注意
        • 加载机制特点
  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • JVM合集
EffectTang
2025-03-23
目录

类加载的过程

# 类加载的过程

了解的JVM跟字节码,或许你会好奇,JVM是怎么读取的字节码文件的,或者说,读取完成后,它主要做了哪些工作,是否有进行优化。

运行 java 程序的第一步就是加载 class 文件/或输入流里面包含的字节码。对应到实际的操作就是,使用命令来运行.class文件,或者jar文件。

java -jar hello.jar
1

Jar 包打开后实际上就等价于一个文件夹,里面有很多 class 文件和资源文件,但是为了方便就打包成 zip 格式。 当然解压了之后照样可以直接用 java 命令来执行。

那之后发生了什么呢?

# 类加载的几个阶段

按照 Java 语言规范和 Java 虚拟机规范的定义, 我们用 “类加载(Class Loading)” 来表示: 将 class/interface 名称映射为 Class 对象的一整个过程。 通俗一些,类加载就是,JVM读取字节码文件到JVM把数据准备好的这个过程。

这个过程(类加载)还可以划分为更具体的阶段: 加载,链接和初始化(loading, linking and initializing)。

类加载的过程均JVM中完成。以下是详细说明,且按顺序依次进行:

  1. 加载(Loading)
  2. 连接(Linking)
    • 验证(Verification)
    • 准备(Preparation)
    • 解析(Resolution)
  3. 初始化(Initialization)

# 加载

加载阶段也可以称为“装载”阶段。 这个阶段主要的操作是: 根据明确知道的 class 完全限定名, 来获取二进制 classfile 格式的字节流,简单点说就是找到文件系统中/jar 包中/或存在于任何地方的“class 文件”。 如果找不到二进制表示形式,则会抛出 NoClassDefFound 错误。

装载阶段并不会检查 classfile 的语法和格式。

或者用网上某个大佬总结的话,

加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。

# 链接

链接可以再次细分为3个阶段。

# 验证/校验

链接过程的第一个阶段是 校验,确保 class 文件里的字节流信息符合当前虚拟机的要求,不会危害虚拟机的安全。

校验过程检查 classfile 的语义,判断常量池中的符号,并执行类型检查, 主要目的是判断字节码的合法性,比如 magic number, 对版本号进行验证。 这些检查过程中可能会抛出 VerifyError, ClassFormatError 或 UnsupportedClassVersionError。

因为 classfile 的验证属是链接阶段的一部分,所以这个过程中可能需要加载其他类,在某个类的加载过程中,JVM 必须加载其所有的超类和接口。

如果类层次结构有问题(例如,该类是自己的超类或接口,死循环了),则 JVM 将抛出 ClassCircularityError。 而如果实现的接口并不是一个 interface,或者声明的超类是一个 interface,也会抛出 IncompatibleClassChangeError。

实现方式:由类加载器(ClassLoader)完成,遵循双亲委派模型。

# 准备

然后进入准备阶段,这个阶段将会创建静态字段, 并将其初始化为标准默认值(比如null或者0 值),并分配方法表,即在方法区中分配这些变量所使用的内存空间。

请注意,准备阶段并未执行任何 Java 代码。

例如:

public static int i = 1;

在准备阶段i的值会被初始化为 0,后面在类初始化阶段才会执行赋值为 1;但是下面如果使用 final 作为静态常量,某些 JVM 的行为就不一样了:

public static final int i = 1; 对应常量 i,在准备阶段就会被赋值 1,其实这样还是比较 puzzle,例如其他语言(C#)有直接的常量关键字 const,让告诉编译器在编译阶段就替换成常量,类似于宏指令,更简单。

# 解析

然后进入可选的解析符号引用阶段。 也就是解析常量池,主要有以下四种:类或接口的解析、字段解析、类方法解析、接口方法解析。

简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在 .class 文件中是以符号引用来存储的(相当于做了一个索引记录)。

在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。

加载一个 class 时, 需要加载所有的 super 类和 super 接口。

Java中的多态特性就是发生在这阶段,多态就是所谓的后期绑定,因为之前是符号引用,现在替换成具体的直接引用。但因为有多个实现,不清楚链接哪个,这个时候就需要等待,通过解析,明确具体调用的对象。

# 初始化

JVM 规范明确规定, 必须在类的首次“主动使用”时才能执行类初始化。

初始化的过程包括执行:

  • 类构造器方法
  • static 静态变量赋值语句
  • static 静态代码块

如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化。所以其实在 java 中初始化一个类,那么必然先初始化过 java.lang.Object 类,因为所有的 java 类都继承自 java.lang.Object。

只要我们尊重语言的语义,在执行下一步操作之前完成 装载,链接和初始化这些步骤,如果出错就按照规定抛出相应的错误,类加载系统完全可以根据自己的策略,灵活地进行符号解析等链接过程。 为了提高性能,HotSpot JVM 通常要等到类初始化时才去装载和链接类。 因此,如果 A 类引用了 B 类,那么加载 A 类并不一定会去加载 B 类(除非需要进行验证)。 主动对 B 类执行第一条指令时才会导致 B 类的初始化,这就需要先完成对 B 类的装载和链接。

以上5个阶段就是类加载,但类的生命周期实际还要包括:

  • 使用
  • 卸载

类的生命周期一共是7个大步骤,这里注意区别,别混淆了。

# 类加载器机制

类加载(也就是类加载的第一阶段)过程可以描述为“通过一个类的全限定名 a.b.c.XXClass 来获取描述此类的 Class 对象”,这个过程由“类加载器(ClassLoader)”来完成。这样的好处在于,子类加载器可以复用父加载器加载的类。系统自带的类加载器分为三种:

  • 启动类加载器(BootstrapClassLoader)
  • 扩展类加载器(ExtClassLoader)
  • 应用类加载器(AppClassLoader)

以下是它们三者的详细介绍

  1. 启动类加载器(bootstrap class loader): 它用来加载 Java 的核心类,是用原生 C++ 代码来实现的,并不继承自 java.lang.ClassLoader(负责加载JDK中jre/lib/rt.jar里所有的class)。它可以看做是 JVM 自带的,我们再代码层面无法直接获取到启动类加载器的引用,所以不允许直接操作它, 如果打印出来就是个 null。举例来说,java.lang.String 是由启动类加载器加载的,所以 String.class.getClassLoader() 就会返回 null。但是后面可以看到可以通过命令行参数影响它加载什么。
  2. 扩展类加载器(extensions class loader):它负责加载 JRE 的扩展目录,lib/ext 或者由 java.ext.dirs 系统属性指定的目录中的 JAR 包的类,代码里直接获取它的父类加载器为 null(因为无法拿到启动类加载器)。
  3. 应用类加载器(app class loader):它负责在 JVM 启动时加载来自 Java 命令的 -classpath 或者 -cp 选项、java.class.path 系统属性指定的 jar 包和类路径。在应用程序代码里可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取应用类加载器。如果没有特别指定,则在没有使用自定义类加载器情况下,用户自定义的类都由此加载器加载。

所以开发者编写的类是由哪种加载器进行加载呢?

答案是:应用类加载器。

开发者编写的类(即应用程序类,位于用户类路径 ClassPath 下)默认由 应用程序类加载器(Application ClassLoader,也称系统类加载器) 加载。这是JVM双亲委派模型中的第三层类加载器,负责加载用户代码中的类。

# 其他类加载器(用户自定义类加载器)

刚刚介绍的是系统自带的加载器,除了上述三种核心类加载器,开发者也可以 自定义类加载器(继承ClassLoader类),常见于以下场景:

  • 热部署:如Tomcat为每个Web应用提供独立的WebappClassLoader,实现类隔离和动态加载。
  • 类隔离:避免依赖冲突(如OSGI框架)。
  • 加密加载:动态解密字节码后加载。

# 双亲委派模型的工作流程

那有这么多类加载器,一个类过来了,系统怎么知道它是来自哪里的,用哪种加载器进行加载呢?实时上它不会主动检查类来自哪里,而是通过一种流程来进行解决,也就是我们说的双亲委派机制,当类加载器收到加载请求时,按以下顺序处理:

  1. 先委派给父类加载器加载。
  2. 若父类加载器无法完成(在自己的搜索范围内找不到类),则由当前类加载器加载。

示例:

  • 用户编写一个java.lang.String类:
    • 应用程序类加载器委派给扩展类加载器,再委派给启动类加载器。
    • 启动类加载器发现已加载核心String类,直接返回,用户自定义的String类不会被加载,避免核心类被篡改。

# 类加载器的归属关系总结

类加载器类型 归属 实现方式 典型代表
启动类加载器 JVM核心 C/C++ 加载rt.jar
扩展类加载器 JVM提供 Java 加载ext目录
应用程序类加载器 JVM提供 Java 加载用户ClassPath
自定义类加载器 开发者实现 Java(继承) Tomcat的WebappClassLoader

# 关键区别与注意

  1. JVM内置类加载器 vs 自定义类加载器:
    • JVM内置的类加载器是JVM启动时自动创建的,负责加载基础类库和用户类。
    • 自定义类加载器需显式实例化,用于扩展类的加载来源或实现特殊逻辑。
  2. 双亲委派模型的打破:
    • 默认情况下,所有类加载器遵循双亲委派模型(优先委派父加载器)。
    • 自定义类加载器可通过重写loadClass()方法打破此模型(如Tomcat优先加载Web应用自身的类)。

# 加载机制特点

类加载机制有三个特点:

  1. 双亲委托:当一个自定义类加载器需要加载一个类,比如 java.lang.String,它很懒,不会一上来就直接试图加载它,而是先委托自己的父加载器去加载,父加载器如果发现自己还有父加载器,会一直往前找,这样只要上级加载器,比如启动类加载器已经加载了某个类比如 java.lang.String,所有的子加载器都不需要自己加载了。如果几个类加载器都没有加载到指定名称的类,那么会抛出 ClassNotFountException 异常。
  2. 负责依赖:如果一个加载器在加载某个类的时候,发现这个类依赖于另外几个类或接口,也会去尝试加载这些依赖项。
  3. 缓存加载:为了提升加载效率,消除重复加载,一旦某个类被一个类加载器加载,那么它会缓存这个加载结果,不会重复加载。
上次更新: 2025/04/23, 16:23:16
JVM的作用跟字节码
快捷输入相关

← JVM的作用跟字节码 快捷输入相关→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式