一、堆
Java 堆是 JVM 中所有线程共享的运行时内存区域,用于存放所有对象实例、数组以及类的实例字段值。
在 Java 中:
String str = new String("abc");
new String("abc")
创建的对象就分配在堆中。
1.1 堆的特点
特性 | 说明 |
---|---|
共享区域 | 所有线程共享堆 |
GC 管理 | 垃圾回收器对堆管理最频繁 |
分代模型 | 为提高 GC 性能,堆被划分为新生代/老年代等区域 |
空间大 | 堆是 JVM 管理内存中最大的区域 |
慢速 | 堆分配慢于栈,依赖 GC 清理 |
1.2 堆的内部结构:分代模型
为提升垃圾回收效率,JVM 把堆进一步划分为:
Java 堆
├── 新生代(Young Generation)
│ ├── Eden 区
│ ├── Survivor 0(S0)
│ └── Survivor 1(S1)
└── 老年代(Old Generation)
1)新生代(Young Generation)
创建的对象默认分配在 Eden 区。
大多数对象生命周期很短,会迅速被 GC 回收。
Minor GC 专门清理新生代。
★ 分区说明:
区域 | 用途 |
---|---|
Eden | 对象初始分配区域 |
Survivor 0 / 1 | Eden 回收后活下来的对象会在 S0/S1 之间“拷贝轮换” |
新生代采用 复制算法(Copying GC),避免内存碎片。
2)老年代(Old Generation)
长期存活或大对象被晋升(Promote)到老年代。
老年代 GC 称为 Major GC 或 Full GC。
回收成本较高,需尽量避免频繁触发。
3)大对象直接进入老年代
- 大于阈值(如
PretenureSizeThreshold
)的对象跳过 Eden,直接进老年代。
1.3 对象生命周期在堆中的流转
new → Eden → Survivor → Old
对象创建在 Eden;
如果发生 Minor GC 且对象存活 → 移至 Survivor;
对象在 Survivor 区多次存活后(如 15 次)→ 晋升到老年代;
老年代中对象若仍不可达 → 被 Full GC 清除。
1.4 JVM 参数:堆大小设置
参数 | 说明 |
---|---|
-Xms |
初始堆大小(最小) |
-Xmx |
最大堆大小 |
-Xmn |
新生代大小(包括 Eden 和 Survivor) |
-XX:SurvivorRatio=8 |
Eden:Survivor 比例(8:1:1) |
-XX:PretenureSizeThreshold |
大对象直接进入老年代的阈值 |
示例:
java -Xms512m -Xmx1024m -Xmn256m -XX:+PrintGCDetails MyApp
1.5 垃圾回收(GC)与堆的关系
GC 主要在堆中进行回收:
类型 | 清理区域 | 特点 |
---|---|---|
Minor GC | 新生代 | 频繁且速度快 |
Major GC / Full GC | 老年代 + 方法区 | 代价高,暂停时间长 |
G1 GC | 跨代混合 | 划分 Region、低延迟、高性能 |
1.6 堆的内存溢出(OutOfMemoryError)
常见堆异常:
java.lang.OutOfMemoryError: Java heap space
原因可能有:
对象持续创建,无法被回收(引用泄漏)
JVM 堆太小
大对象过多
死循环缓存引用
解决方法:
分析堆快照(工具:JVisualVM、MAT)
优化对象生命周期
增大堆(如:
-Xmx2g
)
1.7 性能优化与调优策略
场景 | 建议 |
---|---|
Minor GC 频繁 | 增加新生代大小 -Xmn |
Full GC 频繁 | 减少老年代对象生成,优化代码 |
OOM 崩溃 | 提取堆快照分析泄漏源 |
延迟要求高 | 使用 G1 GC、设置暂停时间 -XX:MaxGCPauseMillis |
1.8 逆向与安全分析中堆的作用
应用 | 分析方法 |
---|---|
动态壳、脱壳 | 在堆中查找动态解密后的类/对象 |
内存马植入 | 某些 WebShell、Agent 注入的对象驻留在堆中 |
动态指针解密 | 壳加载的关键结构常存放于堆 |
堆喷射攻击分析 | 堆中构造恶意数据结构,触发漏洞利用 |
模拟器分析 | 查看模拟环境中的堆分布是否与真机一致 |
1.9 实战示例(对象分配与 GC)
public class TestHeap {
public static void main(String[] args) {
byte[] arr1 = new byte[2 * 1024 * 1024]; // 2MB
byte[] arr2 = new byte[2 * 1024 * 1024];
byte[] arr3 = new byte[2 * 1024 * 1024];
byte[] arr4 = new byte[4 * 1024 * 1024]; // 4MB
}
}
java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails TestHeap
分析输出:
新生代 10M,Eden 8M,S0/S1 各 1M;
大于 4M 的对象直接进老年代;
如果 Eden 装不下,就触发 Minor GC。
小结图
Java Heap
├── Young Generation(新生代)
│ ├── Eden(默认 8/10)
│ ├── Survivor0(1/10)
│ └── Survivor1(1/10)
└── Old Generation(老年代)
类型 | 分配对象 | 是否 GC 管理 | 是否共享 | 应用重点 |
---|---|---|---|---|
堆 | new 的所有对象 | 是 | 是 | 逆向脱壳、泄漏检测、优化 |
二、栈
Java 虚拟机栈(Java Stack) 是每个线程私有的内存区域,用于存储方法调用相关信息,包括 局部变量、操作数栈、动态链接、返回地址等。
每创建一个线程,就会创建一个栈;
每调用一个方法,就会创建一个栈帧(Stack Frame);
方法调用结束后,栈帧被销毁,返回上层方法继续执行。
2.1 栈的结构:栈帧(Stack Frame)
一个线程的栈是由一组栈帧组成的,每个栈帧代表一次方法调用过程,包含以下内容:
组成部分 | 作用 |
---|---|
局部变量表 | 存储方法参数和局部变量(int、引用、float...) |
操作数栈 | 存储计算过程中的中间值 |
动态链接 | 当前方法调用其他方法时的符号引用 |
返回地址 | 方法返回后跳转到的字节码位置 |
异常处理表 | 异常时用于跳转 catch 代码块 |
JVM 使用字节码解释器或 JIT 编译器操作这些栈帧。
2.2 举例说明:栈帧如何运作?
public class Test {
public static void main(String[] args) {
int a = 10;
int b = sum(a, 20);
}
public static int sum(int x, int y) {
int z = x + y;
return z;
}
}
执行顺序栈结构如下:
线程启动
└── main 栈帧
└── 调用 sum()
└── sum 栈帧(独立局部变量表、操作数栈)
└── 执行完 → 返回结果 → main 栈帧继续
2.3 局部变量表(Local Variable Table)
是一个线性表,用于存储基本类型变量和引用类型。
索引号从 0 开始,由字节码指令访问(如
iload_0
)。64 位类型(
long
、double
)会占两个槽(slot)。
示例:
int x = 5; // slot 0
String str = "hi"; // slot 1
long l = 123456L; // slot 2 + 3
2.4 操作数栈(Operand Stack)
每个方法的字节码指令执行时用操作数栈作为临时工作空间;
栈式结构:先进后出;
计算表达式时数据会先入栈,然后执行操作。
示例:
int a = 2 + 3;
JVM 字节码:
iconst_2 // 压入 2
iconst_3 // 压入 3
iadd // 弹出两个操作数,加法后结果入栈
istore_1 // 将结果存入局部变量表 slot 1
2.5 方法返回地址 + 异常处理
返回地址:用于指示方法执行完毕后,从哪里继续执行;
异常表:用于当抛出异常时,查找是否有
catch
块处理。
2.6 JVM 参数控制栈大小
每个线程的栈大小可以通过
-Xss
参数设置。栈过小 →
StackOverflowError
栈过大 → 启动线程数减少,可能 OOM(Out Of Memory,即内存溢出错误)
java -Xss512k MyApp
2.7 异常:StackOverflowError
典型场景:递归无终止条件
public class StackTest {
public static void recurse() {
recurse(); // 无限递归
}
}
执行后异常:
Exception in thread "main" java.lang.StackOverflowError
2.8 线程私有性与安全性
每个线程都有独立的栈;
栈不受其他线程干扰;
所以 局部变量天然线程安全!
2.9 栈与逆向工程的联系
场景 | 分析点 |
---|---|
方法调用追踪 | 分析栈帧生成与销毁位置 |
动态调试时追踪参数 | 查看局部变量表、操作数栈内容 |
SOF 崩溃排查 | 找出调用链是否循环 |
异常劫持 | Hook 方法栈帧前后行为 |
虚拟机逃逸分析 | 判断对象是否在栈上分配,可避免堆分配与 GC |
2.10 栈与逃逸分析
JVM 可通过逃逸分析判断对象是否逃离当前方法:
没有逃逸:可以在栈上分配,GC 不再管理
优化方式:标量替换、栈上分配、锁消除
2.11 小结
名称 | 特性 | 内容 | 与堆的区别 |
---|---|---|---|
Java 栈 | 线程私有 | 栈帧(局部变量、操作数栈、返回地址) | 分配在栈上的是临时数据,堆存储持久对象 |
栈帧 | 每次方法调用生成 | 管理方法执行时的数据 | 方法结束自动销毁 |
栈的核心优势与限制
优势 | 限制 |
---|---|
分配速度快 | 容量小 |
生命周期清晰 | 不适合大对象 |
不需要 GC 管理 | 不适合共享数据 |
栈与堆对比
特性 | 栈 | 堆 |
---|---|---|
所属线程 | 线程私有 | 所有线程共享 |
管理方式 | 调用即分配,调用结束即销毁 | 由 GC 管理 |
分配速度 | 快 | 慢 |
存储内容 | 方法调用上下文、局部变量 | new 出来的对象、数组 |
常见错误 | StackOverflowError | OutOfMemoryError |
三、方法区
方法区(Method Area) 是 Java 虚拟机中线程共享的一块内存区域,主要用于存储类的结构信息、静态变量、运行时常量池、JIT 编译代码等元数据。
方法区也被称为:
非堆(Non-Heap)内存;
是 GC 管理的一部分。
3.1 方法区存储的内容
内容类型 | 说明 |
---|---|
类信息(Class Metadata) | 类名、父类、接口、字段、方法结构 |
字节码(Code) | 类中方法的字节码(用于解释执行或 JIT 编译) |
静态变量 | 所有 static 修饰的字段 |
运行时常量池 | 类加载后从 class 文件中解析的常量 |
JIT 编译代码 | 热点代码被 JIT 编译成机器码后缓存 |
3.2 JDK 版本变迁
JDK 7 及以前
方法区实现为 永久代(PermGen)
受
-XX:PermSize
和-XX:MaxPermSize
控制
JDK 8 及以后
永久代被移除,方法区改为 元空间(Metaspace)
元空间位于 本地内存(Native Memory),不再在 JVM 堆中
JDK 11 起
元空间优化:支持动态释放未使用的 class 元信息内存(降低 footprint)
3.3 元空间(Metaspace)结构
元空间组成 | 描述 |
---|---|
Class Metadata | 存储类的结构 |
Constant Pool | 存储类中的运行时常量 |
Intern String Pool | 字符串常量池,存放所有被 intern() 的字符串 |
Static Fields | 类的静态变量(包括 final 静态字段) |
元空间的大小默认由操作系统限制,可用如下参数控制:
-XX:MetaspaceSize=128m // 初始大小
-XX:MaxMetaspaceSize=512m // 最大大小
3.4 方法区 vs 堆 vs 栈 对比
区域 | 是否线程私有 | 存储内容 | 是否 GC 管理 |
---|---|---|---|
方法区 | 否 | 类结构、静态字段、常量池 | 是(部分) |
堆 | 否 | 实例对象 | 是 |
栈 | 是 | 方法调用、局部变量 | 否 |
3.5 方法区中的运行时常量池
每个类有一个对应的常量池,记录编译时生成的各种常量,如:
类名、方法名、字段名
字符串、整型、浮点数字面量
符号引用(Symbolic Reference)
示例(javap -v Hello.class
输出):
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
逆向时可以通过常量池提取方法名、类名等重要信息,即使加了混淆也能找出关键结构。
3.6 方法区中的静态变量(static
)
所有类的静态字段都存在方法区中;
字段在类加载时初始化,仅一份;
可被反射访问修改。
示例:
public class Demo {
public static int counter = 100; // 存放在方法区
}
3.7 GC 与方法区
JDK 8 之后,元空间部分内容(如 unused class metadata)也会被 GC 回收;
GC 主要针对:
废弃的类加载器(ClassLoader)
Class 元信息
示例:
如果频繁生成动态类(如:使用 cglib、Javassist 动态代理),未及时卸载,会导致:
java.lang.OutOfMemoryError: Metaspace
3.8 常见异常:OutOfMemoryError: Metaspace
原因:
创建过多的动态类(如 Spring AOP、Groovy 脚本)
类未卸载(类加载器泄漏)
元空间大小过小
解决方式:
使用
-XX:MaxMetaspaceSize=512m
增大元空间合理设计类加载器结构,避免泄漏
使用
-XX:+ClassUnloading
配合G1
实现类信息卸载
3.9 逆向分析与方法区
1)获取类结构信息
方法区中保存了所有类的结构;
可以用反射、
Instrumentation
、JVMTI 等 API 读取所有已加载类。
Class[] classes = instrumentation.getAllLoadedClasses();
2)动态类加载跟踪
若壳或恶意代码使用自定义
ClassLoader
加载字节码,可以 Hook:defineClass()
、loadClass()
方法
加载后,这些类的元信息会注册进方法区(Metaspace)
3)常量池逆向分析
可提取混淆后的类名、字段名、字符串,辅助代码还原
特别适用于逆向 Android 中的
.dex
→.class
后分析结构还原
3.10 反编译 & 方法区调试建议
工具 | 用途 |
---|---|
javap -v |
查看字节码与常量池 |
JClassLib |
图形化查看 .class 文件结构(含方法区内容) |
JVM TI / JVMTI Agent |
动态注入 agent,监控类加载与方法区变化 |
VisualVM |
查看类加载器、类实例、方法区使用 |
MAT |
分析内存快照中的 class metadata 占用情况 |
3.11 小结
项目 | 方法区(JDK7前) | 元空间(JDK8+) |
---|---|---|
所在区域 | JVM 堆内 | 本地内存 |
管理内容 | 类信息、常量池、静态变量 | 同上 |
可配置参数 | PermSize/MaxPermSize |
MetaspaceSize/MaxMetaspaceSize |
OOM 异常 | PermGen space |
Metaspace |
调优方式 | 增大永久代大小 | 限制元空间最大值,控制类加载量 |
方法区是 类的“大脑”,保存了所有类的结构信息、静态变量与常量池,是动态加载、混淆壳分析、反射攻击、内存马植入的核心落点区域。
四、常量池
常量池是类加载后存储在 JVM 方法区中的一部分数据结构,主要包含:
字面量(如整数、浮点数、字符串等)
符号引用(如类名、字段名、方法名)
用于运行时构造字段、方法的元信息
常量池包括两部分:
名称 | 说明 |
---|---|
编译期常量池(Constant Pool Table) | 编译后 .class 文件中存在的常量池结构 |
运行时常量池(Runtime Constant Pool) | 类加载到 JVM 后,解析 .class 中的常量池转为 JVM 可用结构 |
4.1 常量池在 JVM 内存中的位置
区域 | 存储内容 |
---|---|
方法区的一部分 | 每个类或接口都有一个运行时常量池 |
与 class 文件一一对应 | 每加载一个 class 文件,就有一个对应常量池 |
4.2 常量池存储的内容(分类)
1)字面量常量(Literal Constants)
数值常量:
int
,long
,float
,double
字符串常量:
"hello"
布尔值、null、字符等字面量
int a = 100; // 常量池中记录 100
String s = "abc"; // 字符串常量池中记录 "abc"
2)符号引用(Symbolic References)
类名引用(Class)
字段引用(FieldRef)
方法引用(MethodRef)
接口方法引用(InterfaceMethodRef)
String s = obj.toString();
这里的 toString
会作为符号引用放进常量池,运行时解析为 obj
的具体实现。
4.3 class 文件中常量池结构(字节码视角)
使用 javap -v Hello.class
查看字节码,可以看到常量池:
javap -v Hello.class
示例输出:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Hello
#3 = Utf8 Hello
#4 = Utf8 java/lang/Object
#5 = Utf8 main
#6 = Utf8 ([Ljava/lang/String;)V
常见常量项类型表:
tag | 名称 | 说明 |
---|---|---|
1 | Utf8 |
字符串或符号名 |
3 | Integer |
int 类型 |
4 | Float |
float 类型 |
5 | Long |
long 类型 |
6 | Double |
double 类型 |
7 | Class |
类或接口引用 |
8 | String |
字符串引用 |
9 | Fieldref |
字段引用 |
10 | Methodref |
方法引用 |
12 | NameAndType |
字段/方法名+类型 |
15 | MethodHandle |
Lambda 表达式/动态调用 |
18 | InvokeDynamic |
动态调用指令(Lambda、反射) |
4.4 字符串常量池(String Constant Pool)
字符串是特殊的:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true,因为指向相同常量池对象
字符串常量池在 JVM 启动时初始化;
所有
"字符串"
直接量存放于此;new String("hello")
会创建两个对象:一个在常量池,一个在堆上;可使用
str.intern()
强制把字符串加入常量池或返回已有常量池引用。
4.5 运行时常量池与类加载器
每个 class 文件有独立的常量池;
加载时常量池会被解析并转化为 JVM 内部引用;
可通过
Class.getConstantPool()
或Instrumentation
获取。
4.6 与反射、动态调用关系密切
反射、Lambda 表达式、动态代理等功能,其方法名、类名、字段名都存在常量池中:
Class<?> clazz = Class.forName("com.example.Hello"); // "com.example.Hello" 来自常量池
逆向时,可从常量池中提取这些敏感字符串,帮助快速定位逻辑结构。
4.7 常量池逆向分析实战应用
应用场景 | 分析方式 |
---|---|
代码混淆分析 | 从常量池中提取字段/方法/类名(如:e.a.b.a() → 原名) |
类名/方法名还原 | 提取 Class 、Methodref 字段对照映射表 |
字符串解密入口 | 字符串加密器的加密内容往往来自常量池(如 "encrypted_abc" ) |
静态调用跟踪 | 常量池记录了调用链(MethodRef → NameAndType) |
反射调用恢复 | Class.forName 、Method.invoke 参数常藏在常量池中 |
4.8 查看常量池的工具
工具 | 用途 |
---|---|
javap -v |
官方命令行工具,查看 class 常量池 |
JClassLib |
图形化查看 class 文件结构与常量池 |
CFR / Procyon / fernflower |
反编译并展示结构 |
ASM / Javassist |
程序化修改字节码中的常量池 |
4.9 混淆分析中的常量池提取示例
# 通过 javap 反编一个混淆类,查看方法名、字段名
Class: a.b.c
Methodref: a.b.c.e(III)Ljava/lang/String;
Utf8: "密钥" → 说明解密逻辑在这里
对逆向安全来说,常量池是“解密的金矿”,尤其在反编译被混淆的壳时,常量池未加密往往成为突破口。
4.10 JVM 限制与 OOM 风险
常量池大小有限(65535 项)
极端情况下可以构造
OutOfMemoryError: constant pool full
常见于恶意 class 文件攻击、Fuzzing 工具测试
4.11 小结
内容 | 是否 GC 管理 | 是否共享 | 用于什么 |
---|---|---|---|
常量池整体 | 是 | 是(每类一个) | 存储常量、符号引用 |
字符串常量池 | 是 | 是 | 管理字符串字面量 |
与方法区关系 | 是其一部分 | 是 | 类结构核心 |
与逆向关系 | 高度相关 | 是 | 提取混淆/解密入口 |
常量池是 class 文件的“元数据仓库”,是逆向定位结构、还原逻辑、解密混淆、理解类加载机制的基础入口。
五、直接内存
直接内存是指 JVM 堆外的一块内存区域,由操作系统分配,不属于 Java 堆,也不在方法区,但受 JVM 管理和限制。
它主要通过:
java.nio.ByteBuffer.allocateDirect(...)
Unsafe.allocateMemory(...)
MappedByteBuffer
(内存映射文件)
进行分配和访问。
5.1 直接内存 vs JVM 堆内存
对比点 | JVM 堆内存 | 直接内存 |
---|---|---|
所在位置 | JVM 管理(堆内) | 系统内存(堆外) |
GC 是否管 | 是 | 否(手动或隐式回收) |
分配方式 | new |
ByteBuffer.allocateDirect() 、Unsafe |
性能 | 有 GC 影响 | 高速、适合大数据传输 |
用途 | 存储普通对象 | 存储大量数据、高性能 I/O 缓冲区 |
5.2 为什么使用直接内存?
高性能 I/O 的关键:避免 JVM 中对象 → 本地内存的来回复制
在使用传统堆内内存时:
数据先从磁盘 → native buffer → JVM 堆中对象
而使用直接内存:
数据从磁盘直接读入堆外内存,省去中间拷贝
优点:
零拷贝(Zero-Copy)能力
避免 GC 影响
文件内存映射(提升读写效率)
5.3 直接内存的使用方式
1)使用 NIO ByteBuffer 分配
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
分配 1KB 的堆外内存
由 JVM 封装底层
malloc
,内存交由操作系统管理不属于 Java 堆,不会被 GC 自动清理,由 JVM 内部回收器(Cleaner)管理
2)使用 Unsafe 分配(更底层)
Unsafe unsafe = getUnsafe(); // 获取方式略麻烦
long address = unsafe.allocateMemory(1024); // 分配 1024 字节
unsafe.putByte(address, (byte) 1);
unsafe.freeMemory(address); // 记得释放!
更接近操作系统底层
若未释放会造成堆外内存泄漏
3)使用 MappedByteBuffer 进行文件内存映射
FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel();
MappedByteBuffer mapped = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
mapped.put(0, (byte) 65); // 修改文件内容,A 对应 65
OS 将文件内容映射到内存地址空间
修改
mapped
相当于直接修改磁盘文件广泛用于数据库、高频交易系统、搜索引擎等
5.4 直接内存与 JVM 参数控制
虽然不在堆中,但 JVM 会尝试限制其最大使用量:
-XX:MaxDirectMemorySize=512m
默认行为:
如果未设置该参数,JVM 会自动设置为
堆最大值
(如-Xmx
)大小
5.5 直接内存泄漏与异常
若未及时释放:
java.lang.OutOfMemoryError: Direct buffer memory
常见原因:
原因 | 说明 |
---|---|
使用 allocateDirect 分配后未释放 | Cleaner 没来得及清理 |
使用 Unsafe 分配未调用 freeMemory | 纯手动内存管理失误 |
内存映射文件使用后未 unmap | 某些 JVM 无法及时 GC unmap |
5.6 直接内存与 GC 的关系
堆外内存不会被 GC 直接管理;
但直接内存对象(如 DirectByteBuffer)在 Java 对象中持有 Cleaner 引用;
当 DirectByteBuffer 被 GC 回收后,Cleaner 会释放对应 native 内存。
若对象长时间引用未释放,则直接内存无法回收,导致内存泄漏。
5.7 实际应用场景
应用场景 | 用途 |
---|---|
Netty 网络框架 | 使用 DirectBuffer 提高 I/O 性能 |
Kafka 消息系统 | 零拷贝传输使用直接内存 |
数据库(如 H2) | 使用 MappedByteBuffer 映射文件 |
文件传输/高频交易 | 内存映射文件、堆外缓存池 |
游戏引擎 | 快速纹理加载与显存交互 |
5.8 逆向与安全相关用途
应用场景 | 技术点 |
---|---|
内存反作弊检测 | 游戏或程序会在直接内存中存放加密逻辑、状态标志位 |
壳加载数据 | 一些混淆壳使用 DirectBuffer 存储解密后的 class 字节码 |
动态类注入 | 使用 defineClass 加载从直接内存中读取的 class 数据 |
规避 GC 跟踪 | 恶意代码将关键信息存放在堆外,绕过 JVM 常规监控 |
JNI native 方法调用 | native 方法操作的内存区域常来自直接内存地址(address 参数) |
5.9 工具查看与调试
工具 | 用途 |
---|---|
jcmd <pid> VM.native_memory summary |
查看堆外内存使用情况 |
jmap |
可查看堆内,但无法查看直接内存 |
NMT (Native Memory Tracking) |
原生内存追踪,可查 DirectMemory 占用 |
VisualVM + NMT plugin |
图形界面查看直接内存使用 |
perf / valgrind |
C 层堆外内存使用分析 |
5.10 小结
项目 | 说明 |
---|---|
存储位置 | 堆外(native memory) |
分配方式 | allocateDirect、Unsafe.allocateMemory |
GC 影响 | 不受 GC 管理,需手动或通过 Cleaner 回收 |
使用场景 | 高性能 IO、内存映射、Zero-Copy |
主要风险 | 内存泄漏、OOM、GC 无法清理 |
监控手段 | jcmd、NMT、VisualVM 插件 |
逆向重点 | 堆外存放关键结构、加密逻辑、绕过 JVM 安全检测机制 |
直接内存是 JVM 高性能 IO 的利器,也是逆向与安全分析中规避 JVM 监管的常用技术手段。
六、类的加载机制(双亲委派模型)
类加载机制负责 将 .class 文件加载到 JVM 并转化为 Class 对象,是 Java 程序运行前的第一步。
Java 类加载过程分为:
【加载】→【验证】→【准备】→【解析】→【初始化】
其中,“加载”这一步中使用了“类加载器”机制(ClassLoader),而其中的核心就是双亲委派模型(Parent Delegation Model)。
6.1 类加载的五个阶段
阶段 | 说明 |
---|---|
加载(Loading) | 读取 .class 文件为字节流并转化为 Class 对象 |
验证(Verification) | 检查字节码合法性、安全性(防止恶意代码) |
准备(Preparation) | 为静态变量分配内存,并赋初始值(非初始化值) |
解析(Resolution) | 将常量池中的符号引用转为直接引用(如方法、类) |
初始化(Initialization) | 执行类的 <clinit> 静态初始化代码块和静态字段赋值 |
6.2 什么是类加载器(ClassLoader)?
类加载器负责加载类的字节码,并生成 Class 对象。JVM 中类由类加载器标识,类名 + 加载器 才能唯一确定一个类。
所以:两个类名相同但由不同 ClassLoader 加载,它们是两个不同的类。
6.3 双亲委派模型(Parent Delegation Model)
核心思想:先让父类加载器尝试加载,如果父类加载失败,才由当前加载器尝试加载。
即:
当前类加载器 → 委托父加载器 → 逐级向上 → 直到 Bootstrap ClassLoader → 没找到才返回尝试自己加载
好处:
防止重复加载
防止核心类库被恶意篡改(如替换 java.lang.String)
6.4 类加载器体系结构
BootstrapClassLoader
↑
ExtClassLoader(Extension)
↑
AppClassLoader(System)
↑
自定义 ClassLoader
加载器 | 说明 |
---|---|
Bootstrap ClassLoader | C++ 实现,加载 rt.jar (JDK核心类) |
ExtClassLoader | 加载 jre/lib/ext/*.jar |
AppClassLoader | 加载 classpath 中的类 |
用户自定义加载器 | 自定义逻辑(插件系统、热部署、加壳) |
6.5 双亲委派流程图
请求加载类 String
↓
自定义类加载器(找父)
↓
AppClassLoader → ExtClassLoader → Bootstrap(找到 String.class)
↑ 加载成功,返回
只有当父类找不到时,才会由子加载器亲自尝试。
6.6 实战示例:自定义类加载器
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassFromDisk(name); // 你自定义的加载逻辑
return defineClass(name, data, 0, data.length);
}
}
可用于加密类解密、插件隔离、JVM内存马注入等。
6.7 打破双亲委派:沙箱逃逸 & 热部署核心
正常 ClassLoader 遵守双亲委派;
某些框架(如 OSGi、Tomcat、JSP 热加载)会打破双亲委派模型,实现类隔离与版本控制。
如何打破?
不调用
super.loadClass()
,直接使用findClass()
加载自己的类;自定义类加载器加载的类不能访问父加载器的类,避免污染。
6.8 典型应用场景
场景 | 类加载行为 |
---|---|
插件系统(IDEA 插件) | 每个插件使用独立的 ClassLoader |
热部署 | 每次热加载都创建新的 ClassLoader(旧的等 GC 回收) |
加密壳加载 | 使用自定义加载器解密字节流再 defineClass() |
沙箱安全机制 | 利用加载器隔离类、控制访问权限 |
JDBC Driver | SPI 加载 JDBC 驱动时,使用线程上下文类加载器加载 |
Servlet/Tomcat | 每个 WebApp 使用独立加载器,防止类污染 |
6.9 逆向与安全实战中的应用
应用点 | 示例说明 |
---|---|
脱壳 & 加密 | 类被自定义加载器动态加载(无法在磁盘找到 class) |
字节码分析 | 需要手动 trace defineClass 的调用栈 |
动态注入 | 使用 defineClass 将字节码注入 JVM(如内存马) |
沙箱逃逸检测 | 是否加载非 core class 中的类(绕过双亲委派) |
类隔离逃逸 | 判断当前类是否由 AppClassLoader 加载 |
6.10 小结
项目 | 说明 |
---|---|
双亲委派模型 | 先委托父加载器加载,父找不到再自己加载 |
优点 | 安全性高、防止重复加载 |
加载器层次结构 | Bootstrap → Ext → App → 自定义 |
自定义加载器用途 | 插件隔离、解密壳、热部署、内存马注入 |
逆向常识 | ClassLoader 是壳的重要点,Class + Loader 决定类唯一性 |
双亲委派模型是 JVM 保证类加载安全的第一道防线,而破坏双亲委派机制,正是加壳、反射攻击、热部署的关键入口。
七、字节码结构(.class 文件分析)
.class
是 Java 编译器生成的中间产物,它不是源代码,也不是机器码,而是 JVM 能识别的一种 平台无关的二进制格式,其核心是字节码(bytecode)+ 元信息。
JVM 规范对 .class
文件的结构做了非常严格的定义,使得 逆向工程、编译器开发、AST 替换、反混淆处理成为可能。
7.1 .class 文件结构总览
按顺序排列如下结构:
| 魔数 Magic Number → 固定 0xCAFEBABE
| 次版本号 Minor Version
| 主版本号 Major Version
| 常量池 Constant Pool
| 访问标志 Access Flags
| 类索引 This Class
| 父类索引 Super Class
| 接口索引表 Interfaces
| 字段表 Fields
| 方法表 Methods
| 属性表 Attributes(包括 Code、LineNumber、SourceFile 等)
7.2 结构字段详细说明
1)魔数(Magic Number)
固定值:
0xCAFEBABE
作用:识别是否为合法的 Java class 文件
2)版本号(Minor + Major)
表示 Java 版本:
Java 8 → 52
Java 11 → 55
Java 17 → 61
Minor Version: 0
Major Version: 52 (JDK 1.8)
3)常量池(Constant Pool)
存储字面量、类名、字段、方法、符号引用等
使用索引访问
表项格式:tag + data(长度不固定)
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
在混淆逆向中,常量池可以还原方法名、类名、调用关系。
4)访问标志(Access Flags)
- 表示类的修饰属性,如 public、final、interface 等
值 | 标志 |
---|---|
0x0001 |
public |
0x0020 |
super(特殊调用方式) |
0x0200 |
interface |
0x0400 |
abstract |
Access flags: 0x0021 (public, super)
5)类索引(This Class)
常量池中的一个 Class 类型项
表示当前类的类名
6)父类索引(Super Class)
指向常量池中父类名称
若是
java.lang.Object
则为0
7)接口表(Interfaces)
- 当前类实现的接口集合,索引到常量池中 Class 类型
Interfaces count: 1
#25 = Interface java/io/Serializable
8)字段表(Fields)
所有成员变量(不包括局部变量)
包括访问标志、名称索引、描述符索引、属性(如 ConstantValue)
private static final int counter = 100;
可映射为:
Field:
name: counter
descriptor: I
access: 0x001A (private, static, final)
ConstantValue: 100
9)方法表(Methods)
所有方法定义,包括构造器
<init>
、静态块<clinit>
每个方法包括:
访问标志
名称
描述符(如
(I)V
表示参数为 int、返回 void)属性(最关键:Code 字节码)
Method:
name: main
descriptor: ([Ljava/lang/String;)V
Code:
0: getstatic
3: ldc "hello"
5: invokevirtual println
10)属性表(Attributes)
存储附加信息,如:
Code
:方法字节码LineNumberTable
:调试信息,源代码行号LocalVariableTable
:局部变量名表SourceFile
:源代码文件名Signature
:泛型签名
举例(Code 部分):
Code:
stack=2, locals=1, args_size=1
0: getstatic #2
3: ldc #3
5: invokevirtual #4
每个 Code 中包含:max_stack
、max_locals
、字节码数组
、异常表、Code Attributes
7.3 方法描述符(Method Descriptor)
描述符 | 含义 |
---|---|
I |
int |
J |
long |
Z |
boolean |
Ljava/lang/String; |
String |
(I)V |
参数为 int,返回 void |
()Ljava/lang/String; |
无参返回 String |
7.4 字节码指令集(Opcode)
指令 | 含义 |
---|---|
getstatic |
获取静态字段 |
ldc |
加载常量池字面量 |
invokevirtual |
调用实例方法 |
invokestatic |
调用静态方法 |
return |
void 返回 |
ireturn |
返回 int |
可以使用 javap -c
或 ASMifier
观察字节码。
7.5 用工具分析 .class
文件结构
工具 | 功能 |
---|---|
javap -v Xxx.class |
官方命令行工具,输出完整结构 |
JClassLib |
图形化查看 .class 文件结构与常量池 |
ASM / BCEL / Javassist |
字节码读取 + 修改 |
CFR / Fernflower / Procyon |
反编译为源码 |
HexEditor + class 格式规范 |
手动查看二进制内容 |
7.6 逆向分析与安全用途
用途 | 描述 |
---|---|
混淆还原 | 通过常量池、描述符还原真实类名方法名 |
恶意字节码检测 | 检查字节码结构异常(符号异常、访问标志伪造) |
热加载 & class 注入 | 动态生成 class 结构并用 defineClass() 注入 |
内存马识别 | 识别恶意 class 中的隐藏行为(如反射调用、native) |
壳还原 | 加壳后的 class 解密后结构分析与恢复 |
7.7 完整流程小示例
源代码:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
运行:
javac Hello.java
javap -v Hello.class
输出节选:
Magic: 0xCAFEBABE
Minor version: 0
Major version: 52
Constant pool:
#1 = Methodref #5.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // Hello World
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
7.8 小结
.class
文件是 JVM 的“汇编语言”,理解它就掌握了 Java 字节码编程、逆向、加壳解壳、安全防护的根本钥匙。
八、Java 对象生命周期
Java 对象生命周期的 6 个阶段
new → 初始化 → 使用 → 不再引用 → 等待回收 → 被 GC 清理
或者:
new 创建对象
→ 对象进入 Eden 区
→ 经 Minor GC 后进入 Survivor 区
→ 经多次 Minor GC 后晋升到 Old 区
→ 最终在 Full GC 中被清除
8.1 对象的创建过程
使用 new
关键字创建对象
User u = new User();
JVM 创建对象的底层流程如下:
步骤 | 描述 |
---|---|
类是否已加载 | 没有则触发类加载、验证、准备、初始化 |
分配内存空间 | 在堆中为对象分配一块连续内存(Eden 区) |
对象内存初始化 | 自动将实例字段设置为默认值 |
执行构造函数 | 执行 <init> 方法,设置字段初始值 |
8.2 对象在内存中的存储结构
Java 对象在 JVM 中有如下三部分组成:
区块 | 含义 |
---|---|
对象头 | 包含类元数据指针、哈希码、GC 状态、锁信息等 |
实例数据 | 实际存储的字段值(按继承层级顺序排列) |
对齐填充 | 使对象大小为 8 字节的整数倍 |
8.3 对象的使用与逃逸
对象创建后可以在堆中被多个线程访问;
编译器可能通过逃逸分析优化对象分配位置:
逃逸分析三种情况:
类型 | 说明 | 优化方向 |
---|---|---|
不逃逸 | 只在当前线程和方法中使用 | 可栈上分配 |
方法逃逸 | 传出方法 | 需要堆上分配 |
线程逃逸 | 被多线程共享 | 禁止同步消除 |
public void test() {
User u = new User(); // 无逃逸,可栈分配
System.out.println(u.name); // 也可能标量替换
}
8.4 对象的生命周期与 GC 管理
Java 的 GC 管理采用 可达性分析算法(Reachability Analysis)
对象是否“存活”依赖于是否可从 GC Root 可达。
GC Root 来源:
当前线程栈中的局部变量
静态字段引用
JNI 引用
8.5 对象存活时间与内存区域(分代)
年轻代(Young Generation)
包含 Eden、S0、S1(两个 Survivor 区)
新创建的对象首先进入 Eden 区
Minor GC:年轻代的垃圾回收
Eden → Survivor0 → Survivor1 → 晋升 Old 区
- 一般经过 15 次 Survivor 区复制 后,进入老年代
老年代(Old Generation)
存放生命周期长的对象,或大对象
回收频率低但代价高
被 Full GC 处理
永久代 / 元空间(PermGen / Metaspace)
存放类元信息(class结构、方法、常量池)
Java 8 后永久代被移除,改为 本地内存的 Metaspace
8.6 对象何时被回收(GC 阶段)
条件 | 是否回收 |
---|---|
GC Root 不可达 | 有可能回收(进入 Finalization) |
实现 finalize() |
JVM 可能再次保留引用(仅一次机会) |
被引用计数 | Java 不采用引用计数,采用可达性算法 |
示例:
public class User {
protected void finalize() throws Throwable {
System.out.println("finalize called");
}
}
该方法会在对象第一次被判定为不可达时被调用一次,但并不保证何时调用或是否调用。
8.7 回收阶段示意图
new → Eden
→ S0 → S1(每次 Minor GC)
→ 多次后晋升 Old 区
→ 老年代 GC(Full GC)
→ 不可达 → Finalize
→ 回收 or Rescue(自救)失败 → 被清除
8.8 对象生命周期优化相关
技术 | 说明 |
---|---|
栈上分配 | 小对象可在栈上分配,避免 GC |
标量替换 | 将对象拆为多个值 |
同步消除 | 移除无需加锁的同步代码块 |
TLAB 分配 | 每个线程分配一块私有内存空间(Thread Local Allocation Buffer) |
8.9 逆向 / 调试应用场景
应用点 | 描述 |
---|---|
内存马分析 | 恶意对象存在堆中但 GC Root 可达(永不回收) |
动态类注入 | 注入对象控制生命周期,如 defineClass → 不可达 → 垃圾回收 |
泄漏分析 | 分析对象引用链是否断裂,是否被缓存错误持有 |
JVM Hook | 监控对象构造 / finalize / 回收过程 |
GCLog 分析 | 追踪对象是否在 Minor/Full GC 中被回收 |
8.10 命令与工具支持
工具 / 命令 | 作用 |
---|---|
jmap -histo |
查看堆中对象分布 |
jmap -dump |
导出堆快照 |
MAT |
Eclipse Memory Analyzer,用于分析泄漏、对象生命周期 |
VisualVM |
图形化内存监控、GC 日志分析 |
jvisualgc |
观察各代内存回收活动 |
-XX:+PrintGCDetails |
输出 GC 详细日志 |
-XX:+UseTLAB |
启用线程本地内存池 |
8.11 小结
阶段 | 描述 |
---|---|
创建(new) | 堆上分配、TLAB |
初始化 | 构造函数执行 |
使用中 | 被引用、在 GC Root 可达路径中 |
不再引用 | 引用断裂,进入回收流程 |
Finalization | finalize() (可能)执行一次 |
回收 | 被 GC 移除,堆内存释放 |
Java 对象从创建到销毁遵循“分代收集 + 可达性分析”原则,其生命周期管理是性能调优、GC 调试、JVM 攻击防御的核心。
九、GC 算法
GC 总览:为什么需要垃圾回收?
Java 使用自动内存管理,对象一旦**不可达(GC Root 不可达)**就应被清除。
GC 目的:自动释放无用内存,避免泄漏与崩溃。
Java 使用“分代回收模型”,不同区域采用不同 GC 算法。
9.1 JVM 分代模型
分代 | 特点 | 默认回收算法 |
---|---|---|
年轻代(Young) | 创建新对象,生命周期短,回收频繁 | 复制算法 |
老年代(Old) | 生命周期长,对象大,回收代价高 | 标记-清除 / G1 |
元空间(Metaspace) | 存储类信息(替代永久代) | N/A |
9.2 常见 GC 算法汇总一览表
算法 | 原理 | 是否压缩 | 应用代 | 适用场景 |
---|---|---|---|---|
标记-清除(Mark-Sweep) | 标记可达 → 清除不可达 | ✘不压缩 | 老年代 | 老式垃圾回收器 |
复制算法(Copying) | 将存活对象复制到新区域 | ✔有压缩 | 年轻代 | 对象生命周期短 |
标记-整理(Mark-Compact) | 标记 → 移动对象 → 压缩 | ✔ | 老年代 | 避免碎片 |
G1(Garbage First) | 按区域增量收集,高并发低停顿 | ✔分区压缩 | 所有代 | 大堆 + 多核系统 |
ZGC | 低延迟压缩 GC(<10ms 停顿) | ✔ | 所有代 | 极低延迟场景 |
Shenandoah | 并发压缩 | ✔ | 所有代 | 响应时间极低系统 |
9.3 标记-清除算法(Mark-Sweep)
原理:
标记所有 GC Root 可达的对象;
清除所有未被标记的对象,释放内存;
内存空间不整理,可能产生碎片。
[Live][Dead][Live][Dead] → GC → [Live][ ][Live][ ]
优点:
实现简单,适合老年代对象。
缺点:
碎片化严重 → 堆空间碎片会影响后续分配;
回收时间长,GC 停顿长。
9.4 复制算法(Copying)
原理:
将内存划为两个区域(如 Eden 和 Survivor);
每次 GC 只在一个区域中标记活对象,并复制到另一块;
所有未复制的对象视为垃圾。
[Eden: A, B] → GC → [Survivor: A, B],Eden 清空
优点:
无碎片,因为复制是连续的;
效率高,只处理活对象。
缺点:
空间浪费严重(50% 内存浪费);
复制成本高;
只适合对象存活率低的年轻代。
9.5 标记-整理算法(Mark-Compact)
原理:
标记所有存活对象;
将所有存活对象向一端压缩;
清除未使用内存。
[Live][Dead][Live] → [Live][Live][ ]
优点:
解决了碎片问题;
适合老年代。
缺点:
需要对象移动 → 会更新所有引用 → 成本高;
停顿时间仍然较长。
9.6 G1 GC(Garbage First)
G1 是 Java 9+ 默认 GC(Java 11 更成熟)
原理:
将堆划分为多个 小区域(Region);
每个 Region 可是 Eden、Survivor 或 Old;
使用增量并发标记 + 区域优先清理策略:
优先回收垃圾最多的 Region(Garbage First);
GC 时同时处理年轻代和老年代;
停顿时间可配置:
-XX:MaxGCPauseMillis=200
优点:
并发标记 → 低停顿;
避免碎片;
大堆管理能力强(>4GB);
缺点:
调优复杂;
边界场景下性能不稳定;
不如 ZGC 延迟低。
9.7 ZGC(低延迟 GC)
Java 11+ 支持,JDK 17 成熟,可实现 GC 停顿 < 1ms
原理:
全部使用 并发标记、并发压缩
使用颜色指针 + 读屏障(Read Barrier)实现并发移动对象
特点:
特性 | 值 |
---|---|
停顿时间 | < 1ms(超低) |
最大堆大小 | TB 级别 |
回收方式 | 并发标记、并发复制、并发压缩 |
指令开销 | 比 G1 稍高,但适合低延迟服务 |
缺点:
不支持 32 位 JVM;
只在特定业务适用,如交易系统、实时游戏;
9.8 Shenandoah GC(另一种低延迟 GC)
RedHat 提出,Java 12 后加入正式版本;
类似 ZGC,但使用写屏障(Write Barrier)+ 并发压缩;
停顿时间 ≈ ZGC,性能可能略优。
9.9 选择哪种 GC?
场景 | 推荐 GC |
---|---|
小应用 / 默认服务 | G1(JDK 9+ 默认) |
大内存、响应要求高 | ZGC / Shenandoah |
对象存活短、分配频繁 | Serial / Parallel(复制算法) |
老年代大、碎片多 | CMS(旧)、G1(新) |
高并发服务 | G1 / ZGC |
9.10 GC 参数调优示例
# G1 + 限制停顿时间
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
# ZGC 启用
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
# Shenandoah 启用
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
9.11 如何观察 GC 行为
工具 | 用途 |
---|---|
-Xlog:gc* (JDK 9+) |
输出 GC 详情 |
jstat -gc <pid> |
查看 JVM 各区内存 |
VisualVM |
图形监控 GC 情况 |
GCEasy.io |
分析 GC 日志 |
JFR |
Java Flight Recorder,实时采样 GC 停顿 |
9.12 逆向与安全用途
用途 | 描述 |
---|---|
对象保活 | 使恶意对象不被 GC 清除(通过 GC Root 引用) |
内存马 | 利用 GC 漏洞隐藏 payload 对象 |
GC Hook | 利用 finalize() 或 Cleaner 执行恶意代码 |
shellcode loader | GC Root 悬挂对象持有 native shellcode |
壳类生成 | 加壳后 class 加载与释放配合 GC 操作 |
9.13 小结
从标记清除到 ZGC,Java GC 算法从简单效率走向并发压缩、低延迟高并发,了解这些算法是 JVM 调优、逆向攻防、稳定性保障的基础能力。
十、反编译工具分析
反编译工具的作用是:
将
.class
或.dex
文件还原为近似的 Java 源码;还原类结构、方法名、字段、控制流等;
用于逆向分析、调试、加固绕过、混淆还原、审计等工作。
10.1 工具对比一览表
工具名 | 支持格式 | 输出格式 | 优势 | 劣势 |
---|---|---|---|---|
jadx |
.dex , .apk |
Java + Smali | 安卓反编译一把手 | 对混淆还原较弱 |
javap |
.class |
字节码(JVM 指令) | 官方权威,字节码分析利器 | 不输出源码 |
fernflower |
.class , .jar |
Java 源码 | 支持结构还原、被 IDEA 集成 | 不支持 DEX |
10.2 jadx
– Android DEX 反编译神器
支持输入
.dex
,.apk
,.jar
输出格式
Java 源码
Smali 汇编(类 Dalvik 字节码)
控制流图(可视化)
安装方式
# 安装 jadx
git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
# 或使用 GUI
jadx-gui your.apk
使用示例
jadx -d out your.apk
目录结构:
out/
└── com/example/
MainActivity.java
utils/Encrypt.java
也可使用 jadx-gui
图形化分析,支持:
查找字符串/类/方法
查看 smali 与 Java 双视图
高亮调用链
逆向应用:
APK 破解、加密函数还原、查找 Web API、定位壳结构;
搭配 Frida 定位要 Hook 的 Java 类和方法;
对
.so
中 JNI 函数调用也能反推调用点。
10.3 javap
– 官方字节码查看器(低级分析利器)
输入
.class
文件
输出格式
字节码(JVM 指令)
常量池、方法签名、字段、属性等元信息
常用参数
javap -c Hello # 输出字节码指令
javap -v Hello # 全部详细信息(版本、常量池、方法表、属性等)
javap -p Hello # 显示 private 方法
示例输出(-c)
public static void main(String[] args);
Code:
0: getstatic #2 // System.out
3: ldc #3 // "Hello World"
5: invokevirtual #4 // println
8: return
可查看的内容包括:
JVM opcode(如 getstatic、ldc、invokevirtual)
方法描述符
(Ljava/lang/String;)V
局部变量表、行号表
Code
属性区
逆向用途:
对比反编译源码与真实指令(识别插桩/壳);
分析混淆后的真实调用路径;
拆解 class 加壳、插码逻辑;
自定义 class 构造(配合 ASM);
10.4 fernflower
– 通用 Java class 反编译器(IDEA 默认用它)
是 IntelliJ IDEA / JD-GUI 默认反编译引擎,功能强大。
支持输入:
.class
,.jar
,.zip
使用方式:
1)图形工具:JD-GUI
下载 JD-GUI:
https://github.com/java-decompiler/jd-gui/releases
拖入
.jar
,可浏览 Java 源码支持结构树
支持保存全部源码
.zip
2)命令行方式
git clone https://github.com/fesh0r/fernflower
cd fernflower
java -jar fernflower.jar <input.class> <output_dir>
输出结构:
源码风格接近真实 Java
支持泛型、匿名内部类、try/catch 等还原
限制:
对高强度混淆(如 ProGuard、Allatori)处理有限
不支持 dex
逆向用途:
快速还原 Java 源码;
审计第三方库;
查找加密解密逻辑;
定位反射类加载结构;
脱壳或加壳还原分析基础。
10.5 混合使用建议
任务 | 推荐工具组合 |
---|---|
APK 反编译 | jadx + jadx-gui + Android Studio |
Class 分析 | javap -v + JD-GUI/fernflower |
代码插桩 | javap + ASM + fernflower 还原检查 |
JVM 壳类分析 | javap + JClassLib + hex editor |
Frida Hook 类找点 | jadx 查类路径 + 方法名 + 参数签名 |
10.6 小结
工具 | 类型 | 优势 | 适合用在 |
---|---|---|---|
jadx | APK/Dex 反编译器 | Android 逆向必备,图形好用 | APP 逆向、加固分析 |
javap | 官方字节码分析 | 可读 opcode、结构信息最权威 | 插码/脱壳/字节码修改 |
fernflower | 通用 class 反编译器 | 输出源码完整,结构清晰 | JAR 审计、逆向、学习源码 |
jadx
是 Android DEX 的眼睛,javap
是 JVM 字节码的放大镜,fernflower
是 Java 源码的还原器,三者结合可还原几乎所有 Java 字节码行为与结构。