关于 java:8. Java 内存模型与 JVM 基础

发布于:2025-07-01 ⋅ 阅读:(25) ⋅ 点赞:(0)

一、堆

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 GCFull 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 位类型(longdouble)会占两个槽(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() → 原名)
类名/方法名还原 提取 ClassMethodref 字段对照映射表
字符串解密入口 字符串加密器的加密内容往往来自常量池(如 "encrypted_abc"
静态调用跟踪 常量池记录了调用链(MethodRef → NameAndType)
反射调用恢复 Class.forNameMethod.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_stackmax_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 -cASMifier 观察字节码。

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 字节码行为与结构。


网站公告

今日签到

点亮在社区的每一天
去签到