技术社区
安全培训
技术社群
积分商城
先知平台
漏洞库
历史记录
清空历史记录
相关的动态
相关的文章
相关的用户
相关的圈子
相关的话题
注册
登录
一文学会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
修饰)的内存分配在方法区(元空间)。
●
初始值为类型默认值(如
int
为
0
,
boolean
为
false
,引用类型为
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.jar
、
charsets.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
人收藏
0
人喜欢
转载
分享
0
条评论
某人
表情
可输入
255
字
评论
发布投稿
热门文章
1
Linux Shellcode开发(Stager & Reverse Shell)
2
Windows Shellcode开发(x64 stager)
3
Fuzz挖掘sudo提权漏洞:一次堆溢出如何逆向分析出提权思路
4
1.6K主机全域沦陷实录:从单点突破到域控接管的终极横向渗透链
5
从JDBC MySQL不出网攻击到spring临时文件利用
近期热点
一周
月份
季度
1
Linux Shellcode开发(Stager & Reverse Shell)
2
Windows Shellcode开发(x64 stager)
3
Fuzz挖掘sudo提权漏洞:一次堆溢出如何逆向分析出提权思路
4
1.6K主机全域沦陷实录:从单点突破到域控接管的终极横向渗透链
5
从JDBC MySQL不出网攻击到spring临时文件利用
暂无相关信息
暂无相关信息
优秀作者
1
一天
贡献值:18800
2
T0daySeeker
贡献值:15500
3
1174735059082055
贡献值:15000
4
Yale
贡献值:14000
5
1674701160110592
贡献值:10000
6
MeteorKai
贡献值:9000
7
熊猫正正
贡献值:8000
8
Bu0uCat
贡献值:8000
9
tj
贡献值:8000
10
1341025112991831
贡献值:7000
目录
1、加载
2、链接
2.1 验证
2.2 准备
2.3 解析
符号引用 vs 直接引用
3、初始化阶段
3.1 初始化阶段的核心任务
3.2 触发初始化的条件
4、类加载器
Bootstrap ClassLoader(启动类加载器)
Extension ClassLoader(扩展类加载器)
Application ClassLoader(应用类加载器)
类加载器的核心方法
类加载的入口 loadClass
findClass
defineClass 将字节码转换为Class对象
resolveClass 完成类的解析
5、 双亲委派模型
特别情况
Tomcat 的 Web 应用类加载器(WebAppClassLoader)
SPI机制
SPI 的典型应用场景
6、如何编写一个自定义的解密类加载器
编写一个HelloWorld类
编写一个Class文件加密代码
编写一个类加载器
使用类加载器加载加密的字节码文件
需要注意的一些问题
类的唯一性
被动引用
接口初始化
类卸载
转载
标题
作者:
你好
http://www.a.com/asdsabdas
文章
转载
自
复制到剪贴板