一文学会Java类加载
pwjcw WEB安全 156浏览 · 2025-04-01 12:39

在java中类加载的过程可分为三个阶段:加载、链接、初始化

1、加载

在加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中, 并映射为JVM认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如jar文件、class文件,甚至是网络数据源等;如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。

加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:

通过一个类的全限定名获取该类的二进制流。

将该二进制流中的静态存储结构转化为方法去运行时数据结构。

在内存中生成该类的 Class 对象,作为该类的数据访问入口。 ==在加载阶段中,可以使用jvm内置的类加载器进行加载,也可以由用户自定义的类加载器进行加载==

2、链接

在连接阶段又可分为三个过程,分别是:验证、准备、解析

2.1 验证

验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机,在该阶段主要完成以下验证

1文件格式验证(魔数0xCAFEBABE、版本号)

2元数据验证(继承final类?抽象方法实现?)

3字节码验证(栈溢出?类型转换合法?)

4符号引用验证(是否存在对应类/方法/依赖)

2.2 准备

准备阶段是正式为类中定义的变量分配内存并设置初始值的阶段 在准备阶段中,部分字段数据会赋值为默认值,也有部分字段数据会直接赋值,具体如下:

静态变量(static修饰)的内存分配在方法区(元空间)。

初始值为类型默认值(如int0booleanfalse,引用类型为null)。

若存在final static常量(编译期已知值),直接赋实际值(如final static int x = 123;)。

2.3 解析

**解析阶段(Resolution)是连接阶段(Linking)的核心步骤之一,主要负责将符号引用(Symbolic References)转换为直接引用(Direct References)**使JVM能够直接定位到内存中的具体目标(如类、字段、方法等)。解析阶段是类加载过程中实现动态绑定和运行时代码执行的关键环节。

符号引用 vs 直接引用

符号引用(Symbolic Reference) 符号引用是一组符号(字符串),用来描述被引用的目标。例如:

类的全限定名(如java.lang.Object

方法名和描述符(如getName()Ljava/lang/String;

字段名和类型(如count:I 符号引用在编译时生成,保存在.class文件的常量池(Constant Pool)中,与内存布局无关。

直接引用(Direct Reference) 直接引用是JVM可以直接使用的内存地址、偏移量或句柄。例如:

指向方法区中类元数据的指针

方法在方法表中的索引

字段在对象内存中的偏移量 直接引用与JVM的内存模型和运行时结构紧密相关。

3、初始化阶段

初始化阶段(Initialization)是类加载过程的最后一步,负责执行类的初始化代码,包括静态变量的显式赋值和静态代码块(static{})的逻辑。此阶段是类真正可用的标志,直接影响程序的运行时行为。以下从核心机制、触发条件、执行顺序、安全风险等方面展开详细说明。

3.1 初始化阶段的核心任务

显式赋值静态变量:为静态变量赋予代码中定义的值(覆盖准备阶段的默认零值)。

执行静态代码块:运行类中所有static{}代码块的逻辑。

初始化父类/父接口:确保父类和父接口(若未初始化)先完成初始化。

线程安全保证:JVM通过锁机制确保多线程环境下类仅初始化一次。

3.2 触发初始化的条件

初始化在类首次被**主动使用(Active Use)**时触发。主动使用的场景包括: 创建类的实例

访问类的静态变量(非final常量)

调用类的静态方法

反射调用类

子类初始化触发父类初始化

程序入口类

在某些场景下不会创建类的初始化 通过子类引用父类的静态字段

通过数组定义类

访问final静态常量(编译期常量)

4、类加载器

Java 类加载器(ClassLoader)是类加载机制的核心组件,负责动态加载类的字节码并生成对应的 Class 对象。类加载器的设计直接影响类的隔离性、安全性以及应用的灵活性,类加载器是在加载阶段进行使用的,以下是java中不同的类加载器以及区别。

Bootstrap ClassLoader(启动类加载器)

Bootstrap ClassLoader由C/C++实现,是JVM的一部分,主要负责加载 JAVA_HOME/lib 目录下的核心类库(如 rt.jarcharsets.jar),它是一个唯一没有父加载器的的类加载器,当一个类的类加载器为Bootstrap ClassLoader时,获取该类的类加载器会为null,如

Extension ClassLoader(扩展类加载器)

该类加载器由java实现,继承自 sun.misc.Launcher$ExtClassLoader,主要负责加载 JAVA_HOME/lib/ext 目录或 java.ext.dirs 系统变量指定路径的扩展类,可以用来加载 JDK 扩展功能(如加密算法、XML 解析器)

Application ClassLoader(应用类加载器)

该类加载器由Java 实现,继承自 sun.misc.Launcher$AppClassLoader用于加载用户类路径(ClassPath)下的类,该类加载器是默认的类加载器。

类加载器的核心方法

类加载的入口 loadClass

loadClass是类加载的入口方法,负责协调双亲委派模型(Parent Delegation Model),它主要负责的功能如下:

1 检查是否已加载:通过findLoadedClass检查类是否已被当前类加载器加载。

2 委派父加载器:若未加载,优先调用父类加载器的loadClass方法。

3 调用findClass:若父类加载失败,调用findClass尝试自行加载。

4 触发解析:根据resolve参数决定是否调用resolveClass完成类的链接。 核心代码如下:

findClass

findClass是类加载器的扩展点,需要子类重写以定义如何从特定来源加载字节码(如文件、网络、加密数据等,它主要负责的功能如下:

1 加载字节码:根据类名定位并读取字节码数据(如.class文件)。

2 调用defineClass:将字节码转换为Class对象。 核心代码:

defineClass 将字节码转换为Class对象

defineClass是JVM提供的底层方法,负责将字节数组转换为Class对象。其核心任务包括:

1 验证字节码格式:确保符合JVM规范(如魔数、版本号)。

2 解析类结构:将字节码转换为方法区的运行时数据结构。

3 生成Class对象:在堆中创建Class实例,作为访问类元数据的入口。

resolveClass 完成类的解析

resolveClass触发类的解析阶段(属于链接阶段的一部分),负责将符号引用转换为直接引用。其核心任务包括:

1 验证类的依赖:确保父类、接口已加载。

2 解析符号引用:将常量池中的类、方法、字段引用解析为内存地址或偏移量。

3 分配内存:为静态变量分配内存(实际在准备阶段完成)。 更多功能方法参考如下表:

方法名
功能描述
调用时机
是否可重写
loadClass(String name, boolean resolve)
类加载的入口方法,实现双亲委派机制: 1. 检查是否已加载。 2. 递归委派父加载器。 3. 调用 findClass() 加载类。
调用 ClassLoader.loadClass() 时触发
可重写(需谨慎,可能破坏双亲委派)
findClass(String name)
实际加载类的逻辑,由子类实现。 从自定义位置(文件、网络等)读取字节码,调用 defineClass() 生成 Class 对象。
loadClass() 委托父加载器失败后调用
必须重写(自定义加载器时)
defineClass(String name, byte[] b, int off, int len)
将字节数组转换为 Class 对象,完成类的定义。 (JVM 内部方法,不可手动调用)
findClass() 中读取字节码后调用
不可重写(final 方法)
resolveClass(Class<?> c)
执行类的链接(Linking),包括验证、准备和解析阶段。 (可选调用,loadClass() 的 resolve 参数控制)
类加载后,需解析时调用(如使用 Class.forName()
可重写(极少需要)
findLoadedClass(String name)
检查类是否已被当前加载器加载,避免重复加载。
loadClass() 第一步调用
可重写(一般无需)
getParent()
返回父类加载器,实现双亲委派层级。
获取类加载器父子关系时调用
不可重写(final 方法)
getResource(String name)
查找资源(如配置文件),遵循类加载器的资源加载规则。
调用 ClassLoader.getResource() 时触发
可重写(自定义资源路径时)
findResource(String name)
实际查找资源的逻辑,由子类实现。
getResource() 委托父加载器失败后调用
可重写(自定义资源加载时)

5、 双亲委派模型

双亲委派模型是Java类加载机制的核心规则,采用层级化的类加载器结构(Bootstrap→Extension→Application)和自底向上的委派逻辑:当一个类加载请求发生时,子加载器不会立即加载,而是逐层向上委托父加载器尝试加载,只有当所有父加载器均无法完成时,子加载器才会自行加载。这种设计避免核心类库(如java.lang.String)被篡改确保类全局唯一性(防止重复加载),同时提升安全性(如阻止恶意代码冒充系统类),但为支持SPI、热部署等场景,可通过线程上下文类加载器(TCCL)或自定义加载器打破委派。

具体的步骤为:

1 向上委派:子加载器不立即加载类,而是递归将请求交给父加载器。

2 父加载器处理

若父加载器能加载,直接返回 Class 对象。

若父加载器无法加载,继续向上委派,直到 Bootstrap 加载器。

1 子加载器兜底:若所有父加载器均无法加载,子加载器才尝试自己加载。 类加载器的核心逻辑在 ClassLoader.loadClass() 方法中:

特别情况

在某些场景中,需要打破双亲委派模型的机制,比如Tomcat中,每个Web应用使用独立的类加载器,优先加载自己目录下的类,而不是直接委托给父加载器,这样可以实现不同Web应用之间的类隔离,避免类冲突,Java的SPI机制中,核心接口由Bootstrap类加载器加载,但具体实现类需要由线程上下文类加载器加载,这需要打破双亲委派。在热部署的场景中,如果每次修改代码后都遵循双亲委派,父加载器已经加载过旧版本的类,子加载器就无法重新加载新类,导致无法热更新。因此,需要自定义类加载器,优先自己加载,绕过父加载器的缓存。

Tomcat 的 Web 应用类加载器(WebAppClassLoader)

Tomcat 的 Web 应用类加载器(WebAppClassLoader)是其实现 多应用隔离 和 热部署 的核心组件,通过打破双亲委派模型的默认规则,解决了 Web 应用中常见的类冲突和版本隔离问题。 WebAppClassLoader 通过重写 loadClass() 方法实现自定义加载逻辑:

SPI机制

Java 的 SPI(Service Provider Interface)机制 是一种服务发现与动态扩展机制,允许框架或核心库定义接口规范,由第三方或用户提供具体实现,并在运行时自动加载这些实现类。SPI 的核心目标是解耦接口与实现

SPI 的典型应用场景

JDBC 驱动加载 核心接口如JDBC的Driver接口位于rt.jar中,由Bootstrap加载器加载。而具体的实现类如MySQL的Driver实现则在应用类路径下,由应用类加载器(AppClassLoader)加载。根据双亲委派模型,父加载器无法加载子加载器的类,因此Bootstrap加载器无法直接加载应用类路径中的实现类,导致SPI无法正常工作。 针对上述问题,场景的解决方法是通过线程上下文类加载器(TCCL) 进行加载 TCCL允许父加载器(Bootstrap)通过 TCCL 主动使用子加载器(如 AppClassLoader)加载类。

具体的流程逻辑为:

关键代码

6、如何编写一个自定义的解密类加载器

编写自定义类加载器通常需要继承ClassLoader类,并重写findClass方法,从指定位置读取类的字节码,然后调用defineClass方法生成Class对象。具体实现代码如下:

编写一个HelloWorld类

编写一个Class文件加密代码

下面类会为指定的Class文件(HelloWorld.class)生成加密后的字节码文件HelloWorld.enc

编写一个类加载器

使用类加载器加载加密的字节码文件

执行过上面的方法之后,就可以看到弹出了计算器,并控制台输出了HelloWorld,但是上面的HelloWorld对象的类加载还是AppClassLoader,而不是TestClassLoader,这是因为双亲委派模型的原因。

需要注意的一些问题

类的唯一性

同一类被不同类加载器加载会被视为不同的类(如MyClass@ClassLoaderA vs MyClass@ClassLoaderB)。

被动引用

某些情况不会触发初始化(如通过子类访问父类静态字段,或通过数组定义类)。

接口初始化

接口的初始化不要求父接口全部初始化(除非主动使用父接口)。

类卸载

类需满足无实例、无Class对象被引用、类加载器可被回收,才能被卸载(由JVM决定)。



0 条评论
某人
表情
可输入 255