关于 java:8. Java 内存模型与 JVM 基础_jvm内存模型
一、堆
Java 堆是 JVM 中所有线程共享的运行时内存区域,用于存放所有对象实例、数组以及类的实例字段值。
在 Java 中:
String str = new String(\"abc\");
-
new String(\"abc\")
创建的对象就分配在堆中。
1.1 堆的特点
1.2 堆的内部结构:分代模型
为提升垃圾回收效率,JVM 把堆进一步划分为:
Java 堆├── 新生代(Young Generation)│ ├── Eden 区│ ├── Survivor 0(S0)│ └── Survivor 1(S1)└── 老年代(Old Generation)
1)新生代(Young Generation)
-
创建的对象默认分配在 Eden 区。
-
大多数对象生命周期很短,会迅速被 GC 回收。
-
Minor GC 专门清理新生代。
★ 分区说明:
新生代采用 复制算法(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
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold
示例:
java -Xms512m -Xmx1024m -Xmn256m -XX:+PrintGCDetails MyApp
1.5 垃圾回收(GC)与堆的关系
GC 主要在堆中进行回收:
1.6 堆的内存溢出(OutOfMemoryError)
常见堆异常:
java.lang.OutOfMemoryError: Java heap space
原因可能有:
-
对象持续创建,无法被回收(引用泄漏)
-
JVM 堆太小
-
大对象过多
-
死循环缓存引用
解决方法:
-
分析堆快照(工具:JVisualVM、MAT)
-
优化对象生命周期
-
增大堆(如:
-Xmx2g
)
1.7 性能优化与调优策略
-Xmn
-XX:MaxGCPauseMillis
1.8 逆向与安全分析中堆的作用
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(老年代)
二、栈
Java 虚拟机栈(Java Stack) 是每个线程私有的内存区域,用于存储方法调用相关信息,包括 局部变量、操作数栈、动态链接、返回地址等。
-
每创建一个线程,就会创建一个栈;
-
每调用一个方法,就会创建一个栈帧(Stack Frame);
-
方法调用结束后,栈帧被销毁,返回上层方法继续执行。
2.1 栈的结构:栈帧(Stack Frame)
一个线程的栈是由一组栈帧组成的,每个栈帧代表一次方法调用过程,包含以下内容:
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 0String str = \"hi\"; // slot 1long l = 123456L; // slot 2 + 3
2.4 操作数栈(Operand Stack)
-
每个方法的字节码指令执行时用操作数栈作为临时工作空间;
-
栈式结构:先进后出;
-
计算表达式时数据会先入栈,然后执行操作。
示例:
int a = 2 + 3;
JVM 字节码:
iconst_2 // 压入 2iconst_3 // 压入 3iadd // 弹出两个操作数,加法后结果入栈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 栈与逆向工程的联系
2.10 栈与逃逸分析
JVM 可通过逃逸分析判断对象是否逃离当前方法:
-
没有逃逸:可以在栈上分配,GC 不再管理
-
优化方式:标量替换、栈上分配、锁消除
2.11 小结
栈的核心优势与限制
栈与堆对比
三、方法区
方法区(Method Area) 是 Java 虚拟机中线程共享的一块内存区域,主要用于存储类的结构信息、静态变量、运行时常量池、JIT 编译代码等元数据。
方法区也被称为:
-
非堆(Non-Heap)内存;
-
是 GC 管理的一部分。
3.1 方法区存储的内容
3.2 JDK 版本变迁
JDK 7 及以前
-
方法区实现为 永久代(PermGen)
-
受
-XX:PermSize
和-XX:MaxPermSize
控制
JDK 8 及以后
-
永久代被移除,方法区改为 元空间(Metaspace)
-
元空间位于 本地内存(Native Memory),不再在 JVM 堆中
JDK 11 起
-
元空间优化:支持动态释放未使用的 class 元信息内存(降低 footprint)
3.3 元空间(Metaspace)结构
intern()
的字符串元空间的大小默认由操作系统限制,可用如下参数控制:
-XX:MetaspaceSize=128m // 初始大小-XX:MaxMetaspaceSize=512m // 最大大小
3.4 方法区 vs 堆 vs 栈 对比
3.5 方法区中的运行时常量池
-
每个类有一个对应的常量池,记录编译时生成的各种常量,如:
-
类名、方法名、字段名
-
字符串、整型、浮点数字面量
-
符号引用(Symbolic Reference)
-
示例(javap -v Hello.class
输出):
Constant pool: #1 = Methodref #2.#3 // java/lang/Object.\"\":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // \"\":()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
JVM TI / JVMTI Agent
VisualVM
MAT
3.11 小结
PermSize/MaxPermSize
MetaspaceSize/MaxMetaspaceSize
PermGen space
Metaspace
方法区是 类的“大脑”,保存了所有类的结构信息、静态变量与常量池,是动态加载、混淆壳分析、反射攻击、内存马植入的核心落点区域。
四、常量池
常量池是类加载后存储在 JVM 方法区中的一部分数据结构,主要包含:
-
字面量(如整数、浮点数、字符串等)
-
符号引用(如类名、字段名、方法名)
-
用于运行时构造字段、方法的元信息
常量池包括两部分:
.class
文件中存在的常量池结构.class
中的常量池转为 JVM 可用结构4.1 常量池在 JVM 内存中的位置
4.2 常量池存储的内容(分类)
1)字面量常量(Literal Constants)
-
数值常量:
int
,long
,float
,double
-
字符串常量:
\"hello\"
-
布尔值、null、字符等字面量
int a = 100; // 常量池中记录 100String 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.\"\":()V #2 = Class #16 // Hello #3 = Utf8 Hello #4 = Utf8 java/lang/Object #5 = Utf8 main #6 = Utf8 ([Ljava/lang/String;)V
常见常量项类型表:
Utf8
Integer
Float
Long
Double
Class
String
Fieldref
Methodref
NameAndType
MethodHandle
InvokeDynamic
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\"
)Class.forName
、Method.invoke
参数常藏在常量池中4.8 查看常量池的工具
javap -v
JClassLib
CFR
/ Procyon
/ fernflower
ASM
/ Javassist
4.9 混淆分析中的常量池提取示例
# 通过 javap 反编一个混淆类,查看方法名、字段名Class: a.b.cMethodref: a.b.c.e(III)Ljava/lang/String;Utf8: \"密钥\" → 说明解密逻辑在这里
对逆向安全来说,常量池是“解密的金矿”,尤其在反编译被混淆的壳时,常量池未加密往往成为突破口。
4.10 JVM 限制与 OOM 风险
-
常量池大小有限(65535 项)
-
极端情况下可以构造
OutOfMemoryError: constant pool full
-
常见于恶意 class 文件攻击、Fuzzing 工具测试
4.11 小结
常量池是 class 文件的“元数据仓库”,是逆向定位结构、还原逻辑、解密混淆、理解类加载机制的基础入口。
五、直接内存
直接内存是指 JVM 堆外的一块内存区域,由操作系统分配,不属于 Java 堆,也不在方法区,但受 JVM 管理和限制。
它主要通过:
-
java.nio.ByteBuffer.allocateDirect(...)
-
Unsafe.allocateMemory(...)
-
MappedByteBuffer
(内存映射文件)
进行分配和访问。
5.1 直接内存 vs JVM 堆内存
new
ByteBuffer.allocateDirect()
、Unsafe
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
常见原因:
5.6 直接内存与 GC 的关系
-
堆外内存不会被 GC 直接管理;
-
但直接内存对象(如 DirectByteBuffer)在 Java 对象中持有 Cleaner 引用;
-
当 DirectByteBuffer 被 GC 回收后,Cleaner 会释放对应 native 内存。
若对象长时间引用未释放,则直接内存无法回收,导致内存泄漏。
5.7 实际应用场景
5.8 逆向与安全相关用途
defineClass
加载从直接内存中读取的 class 数据5.9 工具查看与调试
jcmd VM.native_memory summary
jmap
NMT
(Native Memory Tracking)VisualVM + NMT plugin
perf / valgrind
5.10 小结
直接内存是 JVM 高性能 IO 的利器,也是逆向与安全分析中规避 JVM 监管的常用技术手段。
六、类的加载机制(双亲委派模型)
类加载机制负责 将 .class 文件加载到 JVM 并转化为 Class 对象,是 Java 程序运行前的第一步。
Java 类加载过程分为:
【加载】→【验证】→【准备】→【解析】→【初始化】
其中,“加载”这一步中使用了“类加载器”机制(ClassLoader),而其中的核心就是双亲委派模型(Parent Delegation Model)。
6.1 类加载的五个阶段
.class
文件为字节流并转化为 Class
对象
静态初始化代码块和静态字段赋值6.2 什么是类加载器(ClassLoader)?
类加载器负责加载类的字节码,并生成 Class 对象。JVM 中类由类加载器标识,类名 + 加载器 才能唯一确定一个类。
所以:两个类名相同但由不同 ClassLoader 加载,它们是两个不同的类。
6.3 双亲委派模型(Parent Delegation Model)
核心思想:先让父类加载器尝试加载,如果父类加载失败,才由当前加载器尝试加载。
即:
当前类加载器 → 委托父加载器 → 逐级向上 → 直到 Bootstrap ClassLoader → 没找到才返回尝试自己加载
好处:
-
防止重复加载
-
防止核心类库被恶意篡改(如替换 java.lang.String)
6.4 类加载器体系结构
BootstrapClassLoader↑ExtClassLoader(Extension)↑AppClassLoader(System)↑自定义 ClassLoader
rt.jar
(JDK核心类)jre/lib/ext/*.jar
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 典型应用场景
defineClass()
6.9 逆向与安全实战中的应用
defineClass
的调用栈defineClass
将字节码注入 JVM(如内存马)6.10 小结
双亲委派模型是 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: 0Major Version: 52 (JDK 1.8)
3)常量池(Constant Pool)
-
存储字面量、类名、字段、方法、符号引用等
-
使用索引访问
-
表项格式:tag + data(长度不固定)
#1 = Methodref #2.#3 // java/lang/Object.\"\":()V#2 = Class #4 // java/lang/Object#3 = NameAndType #5:#6 // \"\":()V
在混淆逆向中,常量池可以还原方法名、类名、调用关系。
4)访问标志(Access Flags)
- 表示类的修饰属性,如 public、final、interface 等
0x0001
0x0020
0x0200
0x0400
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)
-
所有方法定义,包括构造器
、静态块
-
每个方法包括:
-
访问标志
-
名称
-
描述符(如
(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
J
Z
Ljava/lang/String;
(I)V
()Ljava/lang/String;
7.4 字节码指令集(Opcode)
getstatic
ldc
invokevirtual
invokestatic
return
ireturn
可以使用 javap -c
或 ASMifier
观察字节码。
7.5 用工具分析 .class
文件结构
javap -v Xxx.class
JClassLib
.class
文件结构与常量池ASM
/ BCEL
/ Javassist
CFR / Fernflower / Procyon
HexEditor + class 格式规范
7.6 逆向分析与安全用途
defineClass()
注入7.7 完整流程小示例
源代码:
public class Hello { public static void main(String[] args) { System.out.println(\"Hello World\"); }}
运行:
javac Hello.javajavap -v Hello.class
输出节选:
Magic: 0xCAFEBABEMinor version: 0Major version: 52Constant pool: #1 = Methodref #5.#17 // java/lang/Object.\"\":()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 创建对象的底层流程如下:
方法,设置字段初始值8.2 对象在内存中的存储结构
Java 对象在 JVM 中有如下三部分组成:
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 阶段)
finalize()
示例:
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 对象生命周期优化相关
8.9 逆向 / 调试应用场景
8.10 命令与工具支持
jmap -histo
jmap -dump
MAT
VisualVM
jvisualgc
-XX:+PrintGCDetails
-XX:+UseTLAB
8.11 小结
finalize()
(可能)执行一次Java 对象从创建到销毁遵循“分代收集 + 可达性分析”原则,其生命周期管理是性能调优、GC 调试、JVM 攻击防御的核心。
九、GC 算法
GC 总览:为什么需要垃圾回收?
-
Java 使用自动内存管理,对象一旦**不可达(GC Root 不可达)**就应被清除。
-
GC 目的:自动释放无用内存,避免泄漏与崩溃。
-
Java 使用“分代回收模型”,不同区域采用不同 GC 算法。
9.1 JVM 分代模型
9.2 常见 GC 算法汇总一览表
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)实现并发移动对象
特点:
缺点:
-
不支持 32 位 JVM;
-
只在特定业务适用,如交易系统、实时游戏;
9.8 Shenandoah GC(另一种低延迟 GC)
-
RedHat 提出,Java 12 后加入正式版本;
-
类似 ZGC,但使用写屏障(Write Barrier)+ 并发压缩;
-
停顿时间 ≈ ZGC,性能可能略优。
9.9 选择哪种 GC?
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+)jstat -gc
VisualVM
GCEasy.io
JFR
9.12 逆向与安全用途
finalize()
或 Cleaner 执行恶意代码9.13 小结
从标记清除到 ZGC,Java GC 算法从简单效率走向并发压缩、低延迟高并发,了解这些算法是 JVM 调优、逆向攻防、稳定性保障的基础能力。
十、反编译工具分析
反编译工具的作用是:
-
将
.class
或.dex
文件还原为近似的 Java 源码; -
还原类结构、方法名、字段、控制流等;
-
用于逆向分析、调试、加固绕过、混淆还原、审计等工作。
10.1 工具对比一览表
jadx
.dex
, .apk
javap
.class
fernflower
.class
, .jar
10.2 jadx
– Android DEX 反编译神器
支持输入
-
.dex
,.apk
,.jar
输出格式
-
Java 源码
-
Smali 汇编(类 Dalvik 字节码)
-
控制流图(可视化)
安装方式
# 安装 jadxgit clone https://github.com/skylot/jadx.gitcd jadx./gradlew dist# 或使用 GUIjadx-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/fernflowercd fernflowerjava -jar fernflower.jar
输出结构:
-
源码风格接近真实 Java
-
支持泛型、匿名内部类、try/catch 等还原
限制:
-
对高强度混淆(如 ProGuard、Allatori)处理有限
-
不支持 dex
逆向用途:
-
快速还原 Java 源码;
-
审计第三方库;
-
查找加密解密逻辑;
-
定位反射类加载结构;
-
脱壳或加壳还原分析基础。
10.5 混合使用建议
jadx + jadx-gui + Android Studio
javap -v + JD-GUI/fernflower
javap + ASM + fernflower 还原检查
javap + JClassLib + hex editor
jadx 查类路径 + 方法名 + 参数签名
10.6 小结
jadx
是 Android DEX 的眼睛,javap
是 JVM 字节码的放大镜,fernflower
是 Java 源码的还原器,三者结合可还原几乎所有 Java 字节码行为与结构。