总结摘要
总结JVM内存模型与GC调优中相关知识点可能提的问题
针对每个JVM内存模型与GC调优相关问题,我准备了一个“一句话原理 + 一句话源码 + 一句话项目/场景”的结构化回答,体现深度同时展现实战能力。
JVM 内存结构
JVM 运行时数据区包含哪些部分?哪些是线程私有的?
整体架构划分
一句话原理:JVM运行时数据区分为五大核心区域:程序计数器、虚拟机栈、本地方法栈(线程私有),堆、方法区(线程共享),这种划分实现了线程隔离与数据共享的平衡。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // JVM运行时数据区在HotSpot源码中的定义
class JavaThread : public Thread {
private:
JavaFrameAnchor _anchor; // 栈帧管理
oop _threadObj; // 线程对象
JNIHandleBlock* _active_handles; // JNI句柄
}
// 堆的全局唯一实例
class CollectedHeap {
friend class VMStructs;
private:
size_t _capacity; // 堆容量
size_t _used; // 已使用空间
}
// 方法区的实现(元空间)
class Metaspace : public CHeapObj {
friend class VMStructs;
private:
MetaWord* _first_class_loader_data; // 类加载器数据
size_t _capacity_words; // 容量
}
// JVM启动参数对应区域
// -Xms: 堆初始大小
// -Xmx: 堆最大大小
// -Xss: 栈大小
// -XX:MetaspaceSize: 元空间初始大小
|
项目场景:在性能调优时,根据区域特性设置JVM参数:堆内存(-Xms/-Xmx)根据业务数据量调整,栈内存(-Xss)根据方法调用深度设置,元空间(-XX:MetaspaceSize)根据类加载数量配置,实现资源合理分配。
线程私有区域详解
一句话原理:每个线程独立拥有的内存区域包括程序计数器(记录执行位置)、虚拟机栈(存储栈帧)、本地方法栈(支持native方法),它们的生命周期与线程相同,天然线程安全。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // 1. 程序计数器 - 记录字节码执行地址
class PC {
// 在HotSpot中,pc存在线程栈中或寄存器中
address _pc; // 当前执行的字节码地址
}
// 2. 虚拟机栈 - 存储栈帧
// 每个方法调用创建一个栈帧
class JavaFrame {
private:
int _max_locals; // 局部变量表容量
int _max_stack; // 操作数栈深度
StackValueCollection* _locals; // 局部变量
StackValueCollection* _stack; // 操作数栈
ConstantPoolCache* _constants; // 运行时常量池引用
}
// 栈帧结构演示
public void methodA() {
int a = 1; // 局部变量表 slot0 = 1
int b = 2; // 局部变量表 slot1 = 2
int c = a + b; // 操作数栈:push a, push b, add, store c
}
// 3. 本地方法栈 - 执行native方法
// HotSpot将本地方法栈和虚拟机栈合并实现
class NativeJump {
// 保存native方法执行上下文
address _return_pc;
frame _caller; // 调用者栈帧
}
// 查看线程私有区域大小
// jstack pid 查看线程栈
// jstat -gcutil pid 查看GC情况
// jmap -heap pid 查看堆内存
|
项目场景:排查栈溢出(StackOverflowError)时,通过jstack查看线程栈,发现递归调用过深导致栈帧过多。调大-Xss参数或优化递归算法,解决了递归深度大的问题(如文件系统遍历)。
线程共享区域详解
一句话原理:线程共享区域包括堆(存储所有对象实例)和方法区(存储类信息、常量、静态变量),需要处理并发访问,是GC管理的主要区域,也是内存调优的重点。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // 1. 堆 - 对象实例的存储地
public class HeapArea {
// 对象在堆中的布局
// 对象头(Mark Word + 类型指针)
// 实例数据(成员变量)
// 对齐填充(8字节对齐)
// 演示对象分配
Object obj = new Object(); // 对象在堆中
// 引用obj在栈中,指向堆中的对象
}
// 对象头结构(32位JVM)
// Mark Word: 8字节(hashcode、GC分代年龄、锁信息)
// Class Pointer: 4字节(指向方法区的类型信息)
// 数组长度: 4字节(如果是数组对象)
// 2. 方法区 - 类信息、常量、静态变量
public class MethodArea {
// 类信息:类名、访问修饰符、方法字节码
// 运行时常量池:字面量、符号引用
// 静态变量:static修饰的变量
// JIT编译后的代码缓存
private static int staticVar = 10; // 在方法区
public void method() {
String str = "hello"; // 字符串常量在运行时常量池
}
}
// JDK8以后方法区实现为元空间(Metaspace)
// -XX:MetaspaceSize=256m
// -XX:MaxMetaspaceSize=512m
// 查看线程共享区域
// jmap -histo pid 查看堆中对象统计
// jstat -gc pid 1000 查看GC情况
// jcmd pid VM.metaspace 查看元空间
|
项目场景:在排查OOM时,通过jmap分析堆内存,发现某个缓存对象占用过多内存。调整缓存策略为软引用(SoftReference),让GC在内存紧张时自动回收,同时监控元空间避免类加载过多导致溢出。
完整区域详解与实战调优
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
| /**
* JVM运行时数据区完整指南
*/
public class JVMMemoryAreaGuide {
/**
* 1. 五大区域详细对比表
*/
public class AreaComparison {
// 程序计数器(线程私有)
// - 作用:记录当前线程执行的字节码行号
// - 特点:唯一不会OOM的区域
// - 异常:无
// 虚拟机栈(线程私有)
// - 作用:存储局部变量、操作数栈、方法出口
// - 特点:每个方法调用创建一个栈帧
// - 异常:StackOverflowError(递归太深)
// OutOfMemoryError(线程太多)
// 本地方法栈(线程私有)
// - 作用:为native方法服务
// - 特点:HotSpot合并到虚拟机栈
// - 异常:同虚拟机栈
// 堆(线程共享)
// - 作用:存储所有对象实例
// - 特点:GC管理的主要区域,分代管理
// - 异常:OutOfMemoryError(堆内存不足)
// 方法区(线程共享)
// - 作用:存储类信息、常量、静态变量
// - 特点:JDK8后元空间替代永久代
// - 异常:OutOfMemoryError(类太多)
}
/**
* 2. 内存参数配置实战
*/
public class MemoryConfig {
// 电商系统JVM配置示例
// java -server -Xms4g -Xmx4g 堆内存
// -Xss256k 栈大小
// -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m 元空间
// -XX:+UseG1GC 使用G1垃圾收集器
// -XX:MaxGCPauseMillis=200 最大GC停顿时间
// -XX:+HeapDumpOnOutOfMemoryError 发生OOM时dump堆
// -XX:HeapDumpPath=/logs/dump 堆dump路径
public void checkMemoryUsage() {
// 运行时查看内存使用
Runtime runtime = Runtime.getRuntime();
long maxMemory = runtime.maxMemory(); // 最大堆内存
long totalMemory = runtime.totalMemory(); // 当前堆内存
long freeMemory = runtime.freeMemory(); // 空闲堆内存
long usedMemory = totalMemory - freeMemory; // 已使用堆内存
System.out.println("最大堆内存: " + maxMemory / 1024 / 1024 + "MB");
System.out.println("已使用堆内存: " + usedMemory / 1024 / 1024 + "MB");
// 获取内存池信息
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryMXBean.getNonHeapMemoryUsage();
}
}
/**
* 3. 常见问题排查场景
*/
public class ProblemDiagnosis {
// 场景1:StackOverflowError
public void recursiveCall() {
recursiveCall(); // 递归无出口
}
// 解决方案:
// - 检查递归终止条件
// - 调大-Xss参数(默认1M,可调为2M)
// - 递归改循环
// 场景2:Java heap space OOM
public void heapOOM() {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不断分配1M内存
}
}
// 解决方案:
// - jmap -dump 分析哪些对象占用内存
// - 检查是否有内存泄漏
// - 调大堆内存或优化代码
// 场景3:Metaspace OOM
public void metaspaceOOM() {
while (true) {
// 不断创建新类(如动态代理、CGlib)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMClass.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) ->
proxy.invokeSuper(obj, args));
enhancer.create();
}
}
// 解决方案:
// - 增加-XX:MaxMetaspaceSize
// - 检查类加载器泄漏
// - 缓存增强类而非重复创建
// 场景4:直接内存OOM
public void directMemoryOOM() {
ByteBuffer.allocateDirect(1024 * 1024 * 1024); // 分配1G直接内存
}
// 解决方案:
// - -XX:MaxDirectMemorySize 控制大小
// - 及时释放DirectBuffer
}
/**
* 4. 监控工具使用
*/
public class MonitoringTools {
public void monitor() {
// 1. jps:查看Java进程
// jps -l
// 2. jstat:查看GC情况
// jstat -gcutil pid 1000 (每秒打印一次GC信息)
// S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
// 3. jmap:堆内存快照
// jmap -heap pid (查看堆概要)
// jmap -histo pid (查看对象统计)
// jmap -dump:format=b,file=heap.hprof pid (堆dump)
// 4. jstack:线程栈
// jstack pid > thread.dump (导出线程栈)
// 5. VisualVM:可视化监控
// - 监控CPU、内存、线程
// - 分析堆dump
// - 查看GC活动
}
}
/**
* 5. 实战:内存泄漏排查
*/
public class MemoryLeakCase {
private static List<byte[]> leakList = new ArrayList<>();
public void memoryLeakExample() {
// 问题代码:不断添加但不释放
while (true) {
leakList.add(new byte[1024 * 100]); // 100K
Thread.sleep(10);
}
}
public void diagnoseLeak() {
// 步骤1:发现GC频繁,Full GC后内存不降
// jstat -gcutil pid 1000
// 步骤2:查看堆中对象分布
// jmap -histo pid | head -20
// 发现byte[]占用大量内存
// 步骤3:堆dump分析
// jmap -dump:format=b,file=heap.hprof pid
// 用MAT或VisualVM分析
// 找到GC Root引用链
// 步骤4:修复代码
// - 使用弱引用
// - 及时清理
// - 限制集合大小
}
}
/**
* 6. 区域大小估算公式
*/
public class SizeEstimation {
// 堆内存 = 活跃数据量 * (1 + 冗余系数)
// 活跃数据量:业务高峰期存活对象大小
// 冗余系数:通常3-5倍(考虑GC效率)
// 栈内存 = 线程数 * 栈大小
// 500线程 * 1M = 500M
// 元空间 ≈ 类数量 * 10K
// 10000个类 ≈ 100M
// 直接内存 = 堆外缓存 + NIO缓冲区
// 电商系统估算示例:
// 活跃数据:2G
// 堆内存:2G * 4 = 8G
// 线程数:500
// 栈内存:500 * 1M = 500M
// 类数量:20000
// 元空间:20000 * 10K = 200M
// 总内存 ≈ 8G + 0.5G + 0.2G = 8.7G
}
}
/**
* JVM运行时数据区总结表
*
* 区域 线程私有 存储内容 异常类型
* 程序计数器 是 执行地址 无
* 虚拟机栈 是 局部变量/操作数栈 StackOverflowError/OOM
* 本地方法栈 是 native方法 同栈
* 堆 否 对象实例 OOM
* 方法区 否 类信息/常量/静态变量 OOM
*
* 调优要点:
* 1. 堆:控制对象数量,避免泄漏
* 2. 栈:控制递归深度,合理设置线程数
* 3. 元空间:控制类加载数量
* 4. 直接内存:合理使用NIO,及时释放
*/
// 面试金句
// "JVM运行时数据区就像公司的办公空间:
// 程序计数器是'个人工作笔记'(私有),
// 虚拟机栈是'个人办公桌'(私有,放正在处理的工作),
// 堆是'公共仓库'(共享,放所有物品),
// 方法区是'公司档案室'(共享,存放规章制度和公共资料)。
// 在性能调优中,我遇到过堆内存泄漏导致频繁GC,
// 通过jmap和MAT分析找到未关闭的数据库连接对象;
// 也遇到过栈溢出导致递归查询崩溃,通过调大-Xss和优化递归算法解决。
// 理解这些区域的职责和关系,是JVM调优的基础。"
|
堆内存是如何划分的?对象创建的过程是怎样的?
堆内存分代划分
一句话原理:JVM堆采用分代收集理论划分为新生代(Young Generation)和老年代(Old Generation),新生代进一步分为Eden区和两个Survivor区(From/To),比例默认8:1:1,通过对象年龄晋升机制实现高效GC。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // HotSpot堆内存划分源码结构
class GenCollectedHeap : public CollectedHeap {
private:
// 新生代和老年代的生成器
GenRemSet* _rem_set;
Generation* _young_gen; // 新生代
Generation* _old_gen; // 老年代
}
// 新生代具体实现(PSYoungGen)
class PSYoungGen : public CHeapObj {
// Eden区和两个Survivor区的起始地址
MutableSpace* _eden_space; // Eden区
MutableSpace* _from_space; // Survivor From区
MutableSpace* _to_space; // Survivor To区
size_t _eden_capacity; // Eden容量
size_t _survivor_capacity; // Survivor容量
}
// 默认空间比例
// -XX:SurvivorRatio=8 Eden:Survivor = 8:1
// 实际分配:Eden(8) + From(1) + To(1) = 10
// JVM参数配置
// -Xms10g -Xmx10g 堆总大小10G
// -Xmn3g 新生代3G
// -XX:SurvivorRatio=8 Eden 2.4G, From 300M, To 300M
// -XX:MaxTenuringThreshold=15 最大晋升年龄15
|
项目场景:在网关系统中,大量请求对象在Eden区快速创建并回收,存活下来的session对象经过多次Minor GC后晋升到老年代。通过监控GC日志发现晋升过快,调大Survivor空间比例,减少对象过早进入老年代,Full GC次数降低60%。
对象创建完整流程
一句话原理:对象创建经历类加载检查→分配内存(指针碰撞/空闲列表)→内存初始化→设置对象头→执行构造方法五个阶段,JVM通过TLAB(线程本地分配缓冲区)优化并发分配性能。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| // 1. 对象创建字节码指令
public class ObjectCreate {
public static void main(String[] args) {
// new 指令对应五个阶段
Object obj = new Object();
// 字节码:
// new #2 // 1.分配内存
// dup // 复制引用
// invokespecial #3 // 2.调用构造方法
// astore_1 // 3.赋值给引用
}
}
// 2. HotSpot源码中的对象创建流程
// instanceOopDesc* InstanceKlass::allocate_instance(TRAPS)
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc);
ConstantPool* constants = istate->method()->constants();
Klass* entry = constants->klass_at(index, CHECK);
if (entry->is_initialized()) { // 类已初始化
// 1. 对象大小计算
size_t size = entry->size_helper();
// 2. 堆内存分配
oop obj = Colleagues::heap()->obj_allocate(klass, size, CHECK);
// 3. 对象头初始化
obj->set_mark(markOopDesc::prototype());
obj->set_klass(klass());
// 4. 压入操作数栈
SET_STACK_OBJECT(obj, 0);
}
}
// 3. TLAB(线程本地分配缓冲区)实现
class ThreadLocalAllocBuffer : public CHeapObj {
HeapWord* _start; // TLAB起始地址
HeapWord* _top; // 当前分配指针
HeapWord* _end; // TLAB结束地址
size_t _desired_size; // 期望大小
// 在TLAB中分配对象
HeapWord* allocate(size_t size) {
HeapWord* obj = _top;
HeapWord* new_top = obj + size;
if (new_top <= _end) {
_top = new_top;
return obj; // TLAB分配成功
}
return NULL; // TLAB空间不足,去Eden分配
}
}
// 4. CAS失败后的重试机制
HeapWord* CASAllocate(size_t size) {
while (true) {
HeapWord* current_top = _top;
HeapWord* new_top = current_top + size;
if (new_top <= _end) {
// CAS更新top指针
if (Atomic::cmpxchg_ptr(new_top, &_top, current_top) == current_top) {
return current_top;
}
} else {
return NULL;
}
}
}
|
项目场景:在秒杀系统中,瞬时创建大量订单对象。通过启用TLAB(默认开启)并适当调大TLAB大小(-XX:TLABSize=512k),减少线程间的CAS竞争,对象分配效率提升40%,系统吞吐量明显提高。
对象内存布局
一句话原理:对象在堆内存中由三部分组成:对象头(Mark Word + 类型指针)、实例数据(成员变量)、对齐填充(8字节对齐),数组对象额外包含数组长度信息。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| // 1. 对象内存布局(32位JVM)
public class ObjectLayout {
// 对象头:12字节
// - Mark Word: 8字节 (hashcode, GC分代年龄, 锁标志)
// - Class Pointer: 4字节 (指向方法区的类元数据)
// 实例数据:4 + 8 + 2 + 4? 取决于字段类型和顺序
private int a; // 4字节
private long b; // 8字节
private short c; // 2字节
private Object d; // 4字节(引用)
// 对齐填充:保证对象大小是8的倍数
// 总大小:12 + (4+8+2+4=18) = 30,对齐到32字节
}
// 2. 查看对象布局的工具
public class ObjectSize {
public static void main(String[] args) {
// 使用JOL (Java Object Layout)
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// 输出示例:
// OFFSET SIZE TYPE DESCRIPTION
// 0 4 (object header) mark word
// 4 4 (object header) mark word
// 8 4 (object header) class pointer
// 12 4 (loss due to the next object alignment)
// Instance size: 16 bytes
}
}
// 3. 数组对象特有结构
public class ArrayLayout {
int[] array = new int[10];
// 对象头:12字节 (mark word 8 + class pointer 4)
// 数组长度:4字节
// 数组数据:10 * 4 = 40字节
// 对齐填充:12+4+40=56,已8字节对齐
// 总大小:56字节
}
// 4. JVM参数查看对象布局
// -XX:+PrintFieldLayout 打印字段布局
// -XX:FieldsAllocationStyle 字段分配策略
|
项目场景:在缓存优化中,通过JOL分析对象内存占用,发现某些DTO对象因字段顺序不当导致内存浪费。按照"长整型→双精度→整型→短整型→字节→布尔"的顺序重排字段,内存占用减少15%,缓存容量提升。
完整流程与实战调优
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
| /**
* 堆内存与对象创建完整指南
*/
public class HeapObjectGuide {
/**
* 1. 对象创建全流程源码级分析
*/
public class ObjectCreationFlow {
public void createObject() {
// 步骤1:类加载检查
// 当虚拟机遇到new指令时,检查常量池中是否有类的符号引用
// 并检查类是否已加载、解析、初始化
// 步骤2:分配内存
// 指针碰撞(Serial、ParNew等带压缩功能的收集器)
// 空闲列表(CMS这种基于Mark-Sweep的收集器)
// 步骤3:内存初始化
// 将分配到的内存空间初始化为零值(不包括对象头)
// 保证实例字段直接使用(int=0, boolean=false, 引用=null)
// 步骤4:设置对象头
// 设置Mark Word(hashcode=0,age=0,偏向锁=0)
// 设置类型指针指向方法区的类元数据
// 步骤5:执行构造方法
// 按顺序执行:父类构造器 → 实例变量初始化 → 构造代码块 → 构造方法
}
}
/**
* 2. TLAB工作原理
*/
public class TLABPrinciple {
// TLAB分配流程
// 1. 线程首次分配时,从Eden区申请一块空间作为TLAB
// 2. 小对象在TLAB内通过指针碰撞快速分配
// 3. TLAB空间不足时,重新申请新的TLAB
// 4. 大对象直接在Eden区分配(不进TLAB)
// TLAB相关JVM参数
// -XX:+UseTLAB 启用TLAB(默认开启)
// -XX:TLABSize=512k 设置TLAB大小
// -XX:-ResizeTLAB 禁用TLAB动态调整
// -XX:TLABRefillWasteFraction=64 TLAB浪费比例
// 监控TLAB使用情况
// jstat -gcutil pid 1000
// jstat -printcompilation pid
}
/**
* 3. 对象晋升机制
*/
public class ObjectPromotion {
// 对象年龄计数器(对象头中4位,最大15)
// 每熬过一次Minor GC,年龄+1
// 达到阈值(-XX:MaxTenuringThreshold)晋升到老年代
// 动态年龄判断
// 如果Survivor中相同年龄对象总和 > Survivor空间一半
// 年龄≥该值的对象直接晋升
// 大对象直接进入老年代
// -XX:PretenureSizeThreshold=3m 大于3M的对象直接在老年代分配
public void promotionExample() {
byte[] obj1 = new byte[1024 * 1024]; // 1M,年轻代分配
byte[] obj2 = new byte[1024 * 1024 * 5]; // 5M,超过阈值直接老年代
}
}
/**
* 4. GC日志分析
*/
public class GCLogAnalysis {
// 开启GC日志
// -XX:+PrintGCDetails
// -XX:+PrintGCDateStamps
// -Xloggc:/logs/gc.log
// 示例GC日志解读
// 2024-01-15T10:30:45.123+0800: [GC (Allocation Failure)
// [PSYoungGen: 2048K->512K(2560K)]
// 2048K->1024K(8192K), 0.0151234 secs]
// [Times: user=0.02 sys=0.00, real=0.02 secs]
//
// 解读:
// - 新生代:2048K→512K,总容量2560K
// - 堆总:2048K→1024K,总容量8192K
// - 耗时:0.015秒
}
/**
* 5. 实战:内存分配优化案例
*/
public class AllocationOptimization {
// 场景1:频繁Minor GC,晋升过快
// 问题:Survivor空间过小,对象直接晋升老年代
// 优化:调大SurvivorRatio(默认8,调为6)
// -XX:SurvivorRatio=6 Eden:Survivor = 6:1
// 场景2:大对象过多导致频繁Full GC
// 问题:大对象直接在老年代分配,触发Full GC
// 优化:调大PretenureSizeThreshold,让大对象在年轻代分配
// -XX:PretenureSizeThreshold=5m
// 场景3:TLAB浪费严重
// 问题:TLAB空间浪费多,频繁重新申请
// 优化:调整TLAB浪费比例
// -XX:TLABRefillWasteFraction=32
// 场景4:对象年龄设置不当
// 问题:对象过早晋升
// 优化:增大MaxTenuringThreshold
// -XX:MaxTenuringThreshold=15
}
/**
* 6. 内存分配监控
*/
public class AllocationMonitor {
public void monitorAllocation() {
// 1. jstat查看分配速率
// jstat -gcutil pid 1000
// 2. -XX:+PrintTenuringDistribution 打印年龄分布
// Desired survivor size 1048576 bytes, new threshold 15 (max 15)
// - age 1: 102400 bytes, 102400 total
// - age 2: 51200 bytes, 153600 total
// 3. JMAP查看对象统计
// jmap -histo pid | head -20
// 4. 使用JFR记录分配事件
// jcmd pid JFR.start name=AllocationRecording
// jcmd pid JFR.dump filename=recording.jfr
}
}
}
/**
* 堆内存与对象创建总结表
*
* 区域/阶段 作用 关键参数
* Eden 新对象分配 比例默认8
* Survivor From/To 年龄1的对象 比例默认1
* TLAB 线程本地分配缓冲区 TLABSize
* 对象头 Mark Word+类型指针 锁状态存储
* 实例数据 成员变量 字段重排
* 对齐填充 8字节对齐 -
*
* 分配流程:
* TLAB分配 → Eden分配 → 进入Survivor → 晋升老年代
*
* 关键参数:
* -Xmn: 新生代大小
* -XX:SurvivorRatio: Eden/Survivor比例
* -XX:MaxTenuringThreshold: 最大晋升年龄
* -XX:PretenureSizeThreshold: 大对象阈值
*/
// 面试金句
// "堆内存分代设计就像城市的'人口分布':新生代是'年轻人'(朝生夕死),
// 老年代是'老年人'(长期存活)。Eden是'新人口登记处',
// Survivor是'过渡区',经过多次筛选才进入'永久居住区'(老年代)。
// 对象创建过程就像'新生命诞生':类加载是'基因检测',
// 内存分配是'分配产房'(TLAB是VIP产房),
// 对象头初始化是'办理出生证明',
// 构造方法是'父母取名'。
// 在电商大促时,通过调大TLAB和调整Survivor比例,
// 让海量订单对象快速分配回收,系统平稳扛住10倍流量。
// 理解这些细节,才能做好JVM性能调优。"
|
什么是 TLAB?为什么要设计 TLAB?
TLAB核心原理
一句话原理:TLAB(Thread Local Allocation Buffer)是JVM在新生代Eden区为每个线程划分的私有分配缓冲区,线程在TLAB内分配对象无需加锁,通过指针碰撞快速分配,大幅提升并发分配性能。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // TLAB在HotSpot中的实现
class ThreadLocalAllocBuffer : public CHeapObj {
HeapWord* _start; // TLAB起始地址
HeapWord* _top; // 当前分配指针(下一个可用地址)
HeapWord* _end; // TLAB结束地址
size_t _desired_size; // TLAB期望大小
size_t _refill_waste_limit; // 允许浪费的最大空间
// TLAB内部分配对象
HeapWord* allocate(size_t size) {
HeapWord* obj = _top;
HeapWord* new_top = obj + size;
if (new_top <= _end) {
// 指针碰撞:更新top指针即可
_top = new_top;
return obj;
}
return NULL; // TLAB空间不足,需要重新申请
}
}
// 每个Java线程持有自己的TLAB
class JavaThread : public Thread {
ThreadLocalAllocBuffer _tlab; // 线程私有的TLAB
// 分配对象时的优先路径
oop allocate(klass, size) {
HeapWord* obj = _tlab.allocate(size);
if (obj != NULL) {
return (oop)obj;
}
// TLAB分配失败,走慢速路径(Eden全局分配)
return allocate_slow(klass, size);
}
}
|
项目场景:在IM系统的消息推送服务中,每秒创建数十万条消息对象。TLAB让每个处理线程在自己的缓冲区分配内存,避免了并发CAS竞争,对象分配性能提升5倍,消息延迟降低60%。
TLAB设计初衷
一句话原理:设计TLAB的核心目的是解决多线程并发分配的内存竞争问题,通过空间换时间策略,为每个线程预留私有缓冲区,将全局竞争转化为本地分配,是JVM并发性能优化的重要实践。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| // 没有TLAB时的分配场景(全局CAS竞争)
HeapWord* allocate_global(size_t size) {
while (true) {
HeapWord* current_top = _eden_top;
HeapWord* new_top = current_top + size;
if (new_top <= _eden_end) {
// CAS更新全局top指针(高并发下竞争激烈)
if (Atomic::cmpxchg_ptr(new_top, &_eden_top, current_top) == current_top) {
return current_top;
}
// CAS失败重试,CPU空转
} else {
return NULL; // Eden空间不足,触发GC
}
}
}
// 有TLAB的分配场景(本地无锁)
HeapWord* allocate_local(size_t size) {
// 1. 先在TLAB分配(95%以上情况成功,无竞争)
HeapWord* obj = _tlab.allocate(size);
if (obj != NULL) return obj;
// 2. TLAB空间不足,尝试重新申请TLAB
if (size <= _tlab.refill_waste_limit()) {
// 浪费当前TLAB剩余空间,申请新TLAB
refill_tlab();
return _tlab.allocate(size);
}
// 3. 对象太大,直接在Eden分配(进慢速路径)
return allocate_global(size);
}
// TLAB大小动态调整
void ThreadLocalAllocBuffer::resize() {
// 根据线程分配速率动态调整TLAB大小
size_t alloc_rate = compute_allocation_rate();
size_t new_size = alloc_rate * _refill_frequency;
set_desired_size(new_size);
}
// JVM参数控制
// -XX:+UseTLAB 启用TLAB(默认开启)
// -XX:TLABSize=512k 设置固定TLAB大小
// -XX:-ResizeTLAB 禁用动态调整
// -XX:TLABRefillWasteFraction=64 TLAB浪费比例(默认1/64)
|
项目场景:在双11大促前压测时,发现对象分配成为瓶颈,CPU大量消耗在CAS竞争上。启用TLAB后(默认已启用),调整TLAB大小和浪费比例,线程分配冲突减少90%,系统吞吐量提升3倍,顺利通过压力测试。
TLAB工作流程
一句话原理:TLAB遵循分配→回退→重填→慢速分配的完整流程:优先在TLAB分配,空间不足时判断对象大小,小对象浪费剩余空间并申请新TLAB,大对象或TLAB调整失败则回到Eden全局分配。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| // TLAB完整工作流程
public class TLABWorkflow {
// 1. TLAB初始化
void initialize_tlab(Thread thread) {
// 从Eden申请一块空间作为线程的TLAB
HeapWord* start = allocate_from_eden(_desired_size);
thread->_tlab._start = start;
thread->_tlab._top = start;
thread->_tlab._end = start + _desired_size;
}
// 2. TLAB分配流程
void* tlab_allocate(Thread thread, size_t size) {
TLAB* tlab = thread->tlab();
// 快速路径:TLAB内分配
void* obj = tlab->allocate(size);
if (obj != NULL) return obj;
// 慢速路径:TLAB空间不足
return slow_allocate(thread, size);
}
// 3. 慢速分配策略
void* slow_allocate(Thread thread, size_t size) {
TLAB* tlab = thread->tlab();
// 判断是否值得浪费剩余空间
if (size <= tlab->refill_waste_limit()) {
// 浪费剩余空间,申请新TLAB
size_t remaining = tlab->end() - tlab->top();
add_to_wasted(remaining); // 统计浪费空间
// 从Eden申请新TLAB
if (initialize_tlab(thread)) {
// 在新TLAB中分配
return tlab->allocate(size);
}
}
// 对象太大或TLAB申请失败,直接在Eden分配
return allocate_in_eden(size);
}
// 4. 对象晋升或GC时的处理
void handle_tlab_on_gc() {
// GC发生时,清理TLAB
// 存活对象移到Survivor区
// 未使用的TLAB空间回收到Eden
}
}
// TLAB监控数据
// jstat -gcutil pid 1000
// jstat -printcompilation pid
// -XX:+PrintTLAB 打印TLAB使用情况
|
项目场景:在实时风控系统中,规则引擎创建大量短生命周期对象。通过-XX:+PrintTLAB监控发现,TLAB浪费率高达30%,调大TLABRefillWasteFraction参数后,TLAB重填次数减少,对象分配效率提升,规则计算延迟降低40%。
完整实战指南
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
| /**
* TLAB完整实战指南
*/
public class TLABCompleteGuide {
/**
* 1. TLAB相关JVM参数详解
*/
public class TLABParameters {
// -XX:+UseTLAB:启用TLAB(JDK8默认开启)
// 显式关闭:-XX:-UseTLAB(除非特殊测试,否则不建议)
// -XX:TLABSize=512k:设置TLAB初始大小
// 默认值:根据CPU核心数和线程数动态计算
// -XX:-ResizeTLAB:禁用TLAB动态调整
// 默认开启动态调整,根据线程分配速率自动缩放
// -XX:TLABRefillWasteFraction=64
// 允许浪费的空间比例,默认1/64的TLAB空间
// 值越小,越容易浪费空间重填TLAB
// 值越大,越容易直接Eden分配
// -XX:PrintTLAB:打印TLAB使用统计
// 输出线程TLAB分配量、浪费量、重填次数
}
/**
* 2. TLAB监控与分析
*/
public class TLABMonitoring {
public void monitorTLAB() {
// 方法1:通过JVM日志
// -XX:+PrintTLAB -XX:+PrintGCDetails
//
// 输出示例:
// TLAB totals: thrds: 16 refills: 2350 max_size: 524288
// GC TLAB: 活对象: 1024KB, 浪费: 128KB
// 方法2:通过JMX获取TLAB信息
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();
// 方法3:使用jstat查看线程分配情况
// jstat -gcutil pid 1000
}
// 分析指标:
// refills: TLAB重填次数(过高说明TLAB太小)
// waste: TLAB浪费空间(过大说明TLAB浪费严重)
// allocation_rate: 线程分配速率(决定TLAB大小)
}
/**
* 3. 实战优化案例
*/
public class TLABCases {
// 案例1:高并发短生命周期对象
// 场景:每秒创建百万级临时对象
// 问题:TLAB频繁重填,CAS竞争严重
// 优化:调大TLAB初始大小,减少重填
// -XX:TLABSize=1m -XX:-ResizeTLAB
// 案例2:混合大小对象
// 场景:大量128-256K对象混合小对象
// 问题:TLAB剩余碎片多,浪费严重
// 优化:调整浪费比例,减少慢速分配
// -XX:TLABRefillWasteFraction=32
// 案例3:计算密集型应用
// 场景:少量大对象,频繁计算
// 问题:TLAB浪费少,但对象创建少
// 优化:保持默认,TLAB收益不大
// 案例4:响应时间敏感系统
// 场景:要求微秒级响应
// 问题:TLAB分配偶尔慢速路径导致抖动
// 优化:增大TLAB,禁用动态调整
// -XX:TLABSize=2m -XX:-ResizeTLAB
}
/**
* 4. 代码层面优化
*/
public class CodeOptimization {
// 1. 对象池化
// 对于频繁创建的大对象,使用对象池
private static final ObjectPool<byte[]> pool =
new ObjectPool<>(100, () -> new byte[1024 * 512]);
// 2. 线程局部变量
// 使用ThreadLocal缓存重复使用的对象
private static final ThreadLocal<ByteBuffer> threadLocalBuffer =
ThreadLocal.withInitial(() -> ByteBuffer.allocate(1024));
// 3. 避免TLAB浪费
// 将对象大小对齐到8的倍数
// 避免创建略大于TLAB剩余空间的对象
// 4. 批量处理
// 合并小对象为大对象,减少TLAB重填
public void batchProcess() {
List<Record> batch = new ArrayList<>(1000);
// 批量处理而非单个处理
}
}
/**
* 5. TLAB与GC的交互
*/
public class TLABAndGC {
public void onYoungGC() {
// Minor GC发生时:
// 1. 所有TLAB中的存活对象被复制到Survivor区
// 2. TLAB中的死亡对象直接回收
// 3. 未使用的TLAB空间回收到Eden
// 4. 新的TLAB在GC后重新分配
}
public void onFullGC() {
// Full GC发生时:
// 1. 所有线程的TLAB先被清空(对象移到老年代)
// 2. TLAB统计信息重置
// 3. GC后重新计算TLAB大小
}
// 关键点:TLAB只是分配优化,不影响对象布局和GC
// GC时不需要特殊处理TLAB中的对象
}
/**
* 6. 不同JVM版本的差异
*/
public class TLABVersionDiff {
// JDK6:引入TLAB,默认启用
// JDK7:改进TLAB大小动态调整算法
// JDK8:优化TLAB浪费统计
// JDK11:支持TLAB的快速重填
// JDK17:增强TLAB监控,支持更细粒度控制
}
}
/**
* TLAB总结表
*
* 特性 说明
* 所属区域 Eden区(年轻代)
* 作用 线程私有分配缓冲区
* 分配方式 指针碰撞(无锁)
* 主要目的 避免并发分配竞争
* 默认启用 是(-XX:+UseTLAB)
* 动态调整 是(-XX:+ResizeTLAB)
* 浪费比例 1/64(-XX:TLABRefillWasteFraction)
*
* 优势:
* - 无锁分配,高性能
* - CPU缓存友好(线程本地)
* - 减少CAS竞争
*
* 劣势:
* - 内存碎片浪费
* - 大对象仍需全局分配
* - 监控管理开销
*/
// 面试金句
// "TLAB是JVM并发性能优化的经典案例,就像'收银台前的私人购物篮':
// 如果没有TLAB,所有线程都要在同一个'收银台'(Eden区)排队结账(CAS竞争);
// 有了TLAB,每个线程有自己的'购物篮',拿完东西直接走(无锁分配),
// 只有篮子装不下大件商品(大对象)时才需要去收银台。
// 在即时通讯项目中,通过监控TLAB使用情况,我们发现默认配置下
// 某些线程TLAB频繁重填,调整大小后消息处理能力翻倍。
// 理解TLAB,就是理解了JVM'空间换时间'的优化智慧。"
|
什么是 OOM?你遇到过哪些 OOM 场景?如何排查?
OOM核心原理
一句话原理:OOM(OutOfMemoryError)是JVM在垃圾收集无法回收足够内存且无法扩展更多内存时抛出的致命错误,标志着内存资源耗尽,系统无法继续正常运行。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // JVM源码中的OOM触发机制
// hotspot/src/share/vm/gc_implementation/shared/vmGCOperations.cpp
void VM_GC_Operation::doit() {
// 尝试进行垃圾回收
if (!_gc_cause.is_allocation_failure()) {
// 非分配失败GC
return;
}
// 分配失败后尝试GC
if (GC_locker::is_active()) {
// GC被锁,等待
return;
}
// 执行GC
if (!_gc_collected) {
_gc_collected = true;
Universe::heap()->collect_as_vm_thread(_gc_cause);
}
// GC后检查是否还有足够内存
if (!Universe::heap()->ensure_parsability()) {
// 内存仍然不足,抛出OOM
THROW_OOM(error);
}
}
// Java层OOM异常
public class OutOfMemoryError extends VirtualMachineError {
public OutOfMemoryError() {
super();
}
public OutOfMemoryError(String s) {
super(s);
}
}
|
项目场景:在一次线上事故中,系统突然无法处理请求,监控显示频繁Full GC但内存不降,最终抛出OOM。通过堆dump分析,发现是消息队列消费者处理速度慢,导致消息对象堆积在内存中。
常见OOM场景及源码分析
Java堆空间(Java heap space)
一句话原理:堆内存不足是最常见的OOM,通常因对象无法被回收(内存泄漏)或对象太多(内存溢出),如缓存无限制增长、大对象过多、连接未关闭等。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // 场景1:内存泄漏 - 集合类忘记清理
public class MemoryLeakCase {
private static List<byte[]> leakList = new ArrayList<>();
public void leakMethod() {
while (true) {
// 不断添加,但不清理
leakList.add(new byte[1024 * 1024]); // 每次1M
}
}
}
// 场景2:内存溢出 - 大对象过多
public class MemoryOverflowCase {
public void overflowMethod() {
// 创建超大数组
int[] hugeArray = new int[Integer.MAX_VALUE / 2];
}
}
// 场景3:连接未关闭
public class ConnectionLeakCase {
public void queryDatabase() {
Connection conn = null;
try {
conn = dataSource.getConnection();
// 执行查询
} finally {
// 忘记关闭连接,导致ResultSet等对象无法回收
}
}
}
// OOM日志示例
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at com.example.MemoryLeakCase.leakMethod(MemoryLeakCase.java:10)
|
排查经验:某次发现系统运行一周后变慢,最终OOM。通过jmap dump堆内存,用MAT分析发现HashMap$Node占用了90%内存,原来是缓存Map没有设置过期时间,无限增长导致。
一句话原理:方法区内存不足,通常因类加载器泄漏、动态代理生成过多类、JSP热部署等导致元空间无法容纳新加载的类信息。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 场景1:动态代理类过多
public class MetaspaceLeakCase {
public void generateClasses() {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BaseClass.class);
enhancer.setUseCache(false); // 不缓存,每次创建新类
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) ->
proxy.invokeSuper(obj, args));
enhancer.create(); // 不断生成新类,撑满元空间
}
}
}
// 场景2:热部署未清理
public class HotDeployLeakCase {
// 每次重新部署时,旧的类加载器未被回收
// 导致之前加载的类仍然存在元空间中
}
// OOM日志示例
// java.lang.OutOfMemoryError: Metaspace
// at java.lang.ClassLoader.defineClass1(Native Method)
// at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
|
排查经验:测试环境频繁热部署后出现Metaspace OOM,检查发现自定义类加载器未被回收。使用jcmd VM.metaspace命令查看元空间使用情况,确认类加载器泄漏后,优化了热部署清理逻辑。
直接内存(Direct buffer memory)
一句话原理:堆外内存不足,通常因NIO操作中使用DirectByteBuffer但未释放,或被Direct Memory容量限制(-XX:MaxDirectMemorySize)约束。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 场景:DirectBuffer未释放
public class DirectMemoryLeak {
public void allocateDirect() {
while (true) {
// 分配直接内存,但不释放
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// buffer没有调用cleaner.clean()或close()
}
}
}
// 源码中的直接内存限制
public class ByteBuffer {
// 直接内存分配
public static ByteBuffer allocateDirect(int capacity) {
// 检查是否超过MaxDirectMemorySize
if (capacity <= 0)
throw new IllegalArgumentException();
Bits.reserveMemory(capacity, maxMemory); // 检查并预留
return new DirectByteBuffer(capacity);
}
}
// OOM日志示例
// java.lang.OutOfMemoryError: Direct buffer memory
// at java.nio.Bits.reserveMemory(Bits.java:694)
|
排查经验:文件处理服务偶尔OOM,错误是"Direct buffer memory"。通过NMT(Native Memory Tracking)发现直接内存占用持续上升,排查代码发现文件读取后未正确释放ByteBuffer,添加清理逻辑后解决。
GC overhead limit exceeded
一句话原理:JVM花超过98%的时间进行GC,但回收不到2%的内存时抛出此异常,表示系统濒临崩溃,再继续运行也无意义。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // JVM源码中的判断逻辑
bool GCHeap::check_gc_overhead_limit() {
// 检查GC时间比例是否超过阈值
size_t gc_time = _gc_time_accumulated;
size_t total_time = os::elapsed_counter() - _start_time;
if (gc_time * 100 > total_time * GCHeap::_gc_time_ratio) {
// GC时间超过98%
if (gc_time > _last_gc_time) {
// 连续多次GC都超限
return true;
}
}
return false;
}
// OOM日志示例
// java.lang.OutOfMemoryError: GC overhead limit exceeded
// at java.util.HashMap.newNode(HashMap.java:1747)
|
排查经验:某系统响应极慢,GC日志显示频繁Full GC,每次回收效果很差。分析发现有个Map在无限增长,导致每次GC都要扫描巨量对象,最终触发GC overhead limit。
请求数组大小超过VM限制(Requested array size exceeds VM limit)
一句话原理:尝试创建超过JVM允许的最大数组(通常为Integer.MAX_VALUE - 2)时抛出,表明数组申请过大。
一句话源码:
1
2
3
4
5
6
7
8
9
10
| // 触发场景
public class ArraySizeLimit {
public void createHugeArray() {
// 尝试创建超过2^31-2的数组
int[] hugeArray = new int[Integer.MAX_VALUE - 1];
}
}
// OOM日志示例
// Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
|
排查经验:数据处理程序在处理超大文件时OOM,检查代码发现试图将整个文件读入一个byte数组,改为分批读取后解决。
OOM排查方法论
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
| /**
* OOM排查完整指南
*/
public class OOMDiagnosisGuide {
/**
* 1. 快速响应三板斧
*/
public class QuickResponse {
// 第一板斧:查看GC日志
// -XX:+PrintGCDetails -XX:+PrintGCDateStamps
// 第二板斧:获取堆dump
// jmap -dump:format=b,file=heap.hprof <pid>
// 第三板斧:查看系统日志
// tail -f logs/app.log | grep "OutOfMemoryError"
}
/**
* 2. 堆内存OOM排查流程
*/
public class HeapOOMProcess {
public void diagnoseHeapOOM() {
// 步骤1:确认OOM类型
// java.lang.OutOfMemoryError: Java heap space
// 步骤2:获取堆dump
// jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
// 步骤3:使用MAT分析
// - Histogram:查看哪些对象占内存
// - Dominator Tree:查看大对象
// - Leak Suspects:自动泄漏检测
// 步骤4:分析GC Root引用链
// 找到对象为何无法回收
// 步骤5:对照代码排查
// - 集合类是否无限增长
// - 连接是否关闭
// - 缓存是否过大
}
// 分析技巧
// 1. 对比多次dump:看哪些对象持续增长
// 2. OQL查询:select * from java.util.HashMap$Node
// 3. 查看线程栈:谁创建了这些对象
}
/**
* 3. 元空间OOM排查流程
*/
public class MetaspaceOOMProcess {
public void diagnoseMetaspaceOOM() {
// 步骤1:确认OOM类型
// java.lang.OutOfMemoryError: Metaspace
// 步骤2:查看类加载情况
// jstat -class <pid>
// 步骤3:查看元空间使用
// jcmd <pid> VM.metaspace
// 步骤4:分析类加载器
// - 是否有自定义类加载器
// - 是否热部署未清理
// 步骤5:dump metaspace
// -XX:+HeapDumpOnOutOfMemoryError包含Metaspace信息
}
// 常用参数
// -XX:MaxMetaspaceSize=256m
// -XX:MetaspaceSize=128m
// -XX:+PrintMetaspaceStatistics
}
/**
* 4. 直接内存OOM排查流程
*/
public class DirectMemoryOOMProcess {
public void diagnoseDirectMemoryOOM() {
// 步骤1:确认OOM类型
// java.lang.OutOfMemoryError: Direct buffer memory
// 步骤2:启用NMT(Native Memory Tracking)
// -XX:NativeMemoryTracking=summary
// jcmd <pid> VM.native_memory summary
// 步骤3:查看DirectBuffer使用
// - 通过JMX获取BufferPoolMXBean
// 步骤4:代码审查
// - NIO操作是否正确释放DirectBuffer
// - 是否使用Netty等框架
}
// 监控代码
public void monitorDirectBuffer() {
List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(
BufferPoolMXBean.class);
for (BufferPoolMXBean pool : pools) {
if (pool.getName().contains("direct")) {
System.out.println("直接内存使用:" + pool.getMemoryUsed());
}
}
}
}
/**
* 5. 预防性配置
*/
public class PreventiveConfig {
// 必备JVM参数
// -XX:+HeapDumpOnOutOfMemoryError # OOM时自动dump
// -XX:HeapDumpPath=/data/dumps # dump存放路径
// -XX:+PrintGCDetails # 打印GC详情
// -XX:+PrintGCDateStamps # GC时间戳
// 监控告警
// - 堆内存使用率 > 80%
// - GC频率 > 5次/分钟
// - GC耗时 > 1秒/次
// 代码层面
// - 使用WeakHashMap做缓存
// - 及时关闭资源(try-with-resources)
// - 限制集合大小
// - 使用连接池
}
/**
* 6. 实战案例:内存泄漏排查
*/
public class RealCase {
// 现象:系统运行1天后OOM,重启后恢复
// 排查过程:
// 1. 设置-XX:+HeapDumpOnOutOfMemoryError
// 2. 第二天获取heap.hprof
// 3. 用MAT分析发现char[]占用大量内存
// 4. 查找GC Root
// → Thread对象 → 线程局部变量 → StringBuilder
// 5. 代码审查发现:
// ThreadLocal<StringBuilder> tl = new ThreadLocal<>();
// 但未调用remove(),导致每个线程的StringBuilder无法释放
// 6. 修复:finally块中tl.remove()
// 7. 验证:运行一周,内存稳定
}
}
/**
* OOM类型与排查要点总结
*
* OOM类型 原因 排查重点
* Java heap space 堆内存不足 对象泄漏、大对象
* Metaspace 元空间不足 类加载器泄漏
* Direct buffer memory 直接内存不足 DirectBuffer未释放
* GC overhead limit 98%时间GC但效果差 内存泄漏导致GC无效
* Requested array size 数组超限 数组长度控制
*
* 排查工具:
* jmap:堆dump
* jstat:GC监控
* jcmd:元空间/NMT
* MAT/Eclipse Memory Analyzer:堆分析
* VisualVM:实时监控
* Arthas:在线诊断
*/
// 面试金句
// "OOM是JVM的最后一道防线,就像'水库决堤',原因可能是:
// 水库太小(堆内存不足),
// 进水太多(对象创建过快),
// 排水堵塞(内存泄漏)。
// 排查OOM就像侦探破案:GC日志是'案发现场记录',
// 堆dump是'尸体解剖',MAT是'显微镜',
// 代码审查是'寻找凶手'。
// 我经历过最经典的案例是ThreadLocal未清理导致的内存泄漏,
// 通过MAT找到GC Root,发现每个线程都持有一个大对象,
// 修复后系统稳定运行。理解OOM的本质,才能快速定位和解决问题。"
|
垃圾回收(GC)
如何判断对象是否可回收?引用计数法和可达性分析的区别?
引用计数法原理
一句话原理:引用计数法通过给每个对象维护一个计数器,记录被引用的次数,当计数器归零时对象可回收,但无法解决循环引用问题,主流JVM已弃用。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // 引用计数法实现原理(伪代码)
class ReferenceCountObject {
private int refCount = 0; // 引用计数器
public void addRef() {
refCount++; // 被引用时+1
}
public void releaseRef() {
refCount--; // 引用释放时-1
if (refCount == 0) {
// 计数器归零,可以回收
garbageCollect(this);
}
}
}
// 循环引用问题演示
public class CircularReferenceProblem {
public static void main(String[] args) {
class Node {
Node next;
// 计数器初始为1(被变量引用)
}
Node a = new Node(); // a.refCount = 1
Node b = new Node(); // b.refCount = 1
a.next = b; // b.refCount = 2
b.next = a; // a.refCount = 2
a = null; // a.refCount = 1 (被b.next引用)
b = null; // b.refCount = 1 (被a.next引用)
// 计数器永远不为0,无法回收,造成内存泄漏
}
}
// Python中的引用计数(带循环检测)
import gc
# 手动触发循环引用检测
gc.collect()
|
项目场景:在早期脚本语言(如Python、PHP)中曾使用引用计数法,但遇到父子节点互相引用的树形结构(如菜单树、组织架构)时,即使外部不再使用,对象也无法释放,必须额外引入循环检测机制。
可达性分析原理
一句话原理:可达性分析从GC Roots对象出发,通过引用链向下搜索,形成引用链,任何不在引用链上的对象判定为不可达,即可回收,完美解决循环引用问题。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // HotSpot可达性分析实现
classoopDesc* CollectedHeap::obj() {
// 对象头中有标记位用于GC标记
markOop mark = obj->mark();
// 判断是否可达
if (mark->is_marked()) {
return obj; // 已标记,可达
}
return NULL; // 未标记,不可达
}
// GC Roots对象类型
// 1. 虚拟机栈(栈帧中的本地变量表)引用的对象
// 2. 方法区中静态属性引用的对象
// 3. 方法区中常量引用的对象
// 4. 本地方法栈中JNI引用的对象
// 5. Java虚拟机内部的引用(系统类加载器、基本类等)
// 6. 所有被同步锁(synchronized)持有的对象
// JVM源码中的GC Roots枚举
void CodeBlob::oops_do(OopClosure* f) {
// 遍历所有GC Roots
for (oop* p = _oop_begin; p < _oop_end; p++) {
oop obj = *p;
if (obj != NULL && obj->is_oop()) {
f->do_oop(p); // 标记可达对象
}
}
}
// 三色标记算法实现
enum G1CollectedHeap::MarkColor {
white, // 未被标记(不可达)
gray, // 被标记但未扫描引用
black // 被标记且已扫描引用
};
void G1ParScanThreadState::mark_object(oop obj) {
if (obj->mark()->is_white()) {
obj->mark()->set_gray(); // 白色→灰色
_task_queue->push(obj); // 加入扫描队列
}
}
|
项目场景:在微服务架构中,缓存对象可能被多个组件引用形成复杂网络(如订单→用户→地址→订单),可达性分析能从GC Roots(如线程栈中的本地变量)出发,准确判断哪些对象真正存活,避免循环引用导致的内存泄漏。
两种方法深度对比
一句话原理:引用计数法简单高效但缺陷致命,可达性分析复杂精准但需要STW,现代JVM通过三色标记+增量更新等算法优化,在保证准确性的同时降低停顿时间。
一句话源码:
对比实现:引用计数法 vs 可达性分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 1. 引用计数法(Python示例)
import sys
class PyObject:
def __init__(self):
self.ob_refcnt = 1 # 引用计数
def incref(self):
self.ob_refcnt += 1
def decref(self):
self.ob_refcnt -= 1
if self.ob_refcnt == 0:
self.__del__() # 立即回收
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // 2. 可达性分析(JVM示例)
class ReachabilityAnalysis {
// 标记阶段:从GC Roots开始遍历
public void markPhase() {
Stack<oop> markingStack = new Stack<>();
// 将GC Roots压入栈
for (oop root : gcRoots) {
markingStack.push(root);
root.mark = true;
}
// 遍历所有可达对象
while (!markingStack.isEmpty()) {
oop current = markingStack.pop();
for (oop ref : current.getReferences()) {
if (!ref.mark) {
ref.mark = true;
markingStack.push(ref);
}
}
}
}
// 清理阶段:回收未标记对象
public void sweepPhase() {
for (oop obj : heap) {
if (!obj.mark) {
free(obj); // 不可达,回收
} else {
obj.mark = false; // 重置标记,为下次GC准备
}
}
}
}
// 对比表格(源码注释)
// | 特性 | 引用计数法 | 可达性分析 |
// |------------|---------------------|--------------------|
// | 时间复杂度 | O(1)(增减计数) | O(N)(遍历所有对象) |
// | 内存开销 | 计数器(4-8字节) | 标记位(1-2字节) |
// | 循环引用 | 无法处理 | 完美处理 |
// | STW | 无需STW | 需要STW |
// | 实时性 | 立即回收 | 延迟回收 |
|
项目场景:在实时风控系统中,引用计数法可能会导致频繁的对象回收开销,而JVM的可达性分析配合CMS/G1收集器,能在短暂的STW内完成标记,再并发清理,既保证了准确性又兼顾了低延迟要求。
完整原理与实战应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
| /**
* 对象可回收判断完整指南
*/
public class GCRootAnalysis {
/**
* 1. GC Roots详细分类
*/
public class GCRootTypes {
// 代码演示各种GC Roots
private static Object staticObj = new Object(); // 方法区静态引用
private static final String CONST = "constant"; // 方法区常量引用
public void method() {
Object localObj = new Object(); // 虚拟机栈引用
synchronized (localObj) { // 同步锁持有的对象
// ...
}
}
public native void nativeMethod(); // JNI引用的对象
}
/**
* 2. 四种引用类型对回收的影响
*/
public class ReferenceTypes {
// 强引用(Strong Reference)- 永不回收
Object strongRef = new Object();
// 软引用(Soft Reference)- 内存不足时回收
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 弱引用(Weak Reference)- 下次GC就回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 虚引用(Phantom Reference)- 无法通过它获取对象
PhantomReference<Object> phantomRef = new PhantomReference<>(
new Object(), new ReferenceQueue<>());
public void referenceDemo() {
// 软引用适合实现缓存
Object obj = softRef.get();
if (obj == null) {
// 已被回收,重新加载
obj = loadFromDB();
softRef = new SoftReference<>(obj);
}
// 弱引用适合实现规范映射(如WeakHashMap)
WeakHashMap<Key, Value> cache = new WeakHashMap<>();
// 当Key没有强引用时自动被回收
}
}
/**
* 3. finalize()方法与自救
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("我还活着");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize方法执行");
SAVE_HOOK = this; // 自救,重新建立引用
}
public static void main(String[] args) throws Exception {
SAVE_HOOK = new FinalizeEscapeGC();
// 第一次自救
SAVE_HOOK = null;
System.gc();
Thread.sleep(500); // finalize优先级低,等待执行
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive(); // 输出:我还活着
} else {
System.out.println("对象已死");
}
// 第二次自救失败(finalize只执行一次)
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("对象已死"); // 这次死了
}
}
}
/**
* 4. 实战:内存泄漏排查
*/
public class MemoryLeakDiagnosis {
// 案例1:ThreadLocal内存泄漏
public class ThreadLocalLeak {
private static ThreadLocal<byte[]> tl = new ThreadLocal<>();
public void useThreadLocal() {
tl.set(new byte[1024 * 1024]); // 1M
// 忘记调用tl.remove()
}
// ThreadLocalMap中的Entry使用弱引用,
// 但value是强引用,线程不结束就无法回收
}
// 案例2:静态集合类
public class StaticCollectionLeak {
private static List<Object> staticList = new ArrayList<>();
public void addToList(Object obj) {
staticList.add(obj); // 静态引用,永远不会被回收
}
}
// 排查方法
public void diagnoseLeak() {
// 1. jmap -dump:live,format=b,file=heap.hprof <pid>
// 2. 使用MAT分析
// - 查看Histogram,找到占用内存大的对象
// - 右键对象 → Merge Shortest Paths to GC Roots
// - 排除软/弱/虚引用,只看强引用
// 3. 找到引用链后分析代码
}
}
/**
* 5. GC算法中的可达性分析
*/
public class GCAlgorithms {
// CMS收集器:初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清理
// 使用增量更新处理并发标记期间引用变化
// G1收集器:初始标记(STW) → 并发标记 → 最终标记(STW) → 筛选回收
// 使用SATB(Snapshot-At-The-Beginning)保证正确性
// ZGC:染色指针+读屏障,几乎无STW
// 通过指针染色实现并发标记
}
/**
* 6. 面试重点总结
*/
public class InterviewSummary {
// 判断对象是否可回收的两步:
// 1. 可达性分析:从GC Roots出发,不可达则标记可回收
// 2. 第二次标记:如果对象覆盖finalize()且未自救,才真正回收
// 引用计数法的致命缺陷:
// 无法解决循环引用,Python需要额外gc模块处理
// 现代JVM的选择:
// 可达性分析 + 三色标记 + 增量更新/SATB
// 实战经验:
// - WeakHashMap解决缓存内存泄漏
// - ThreadLocal使用后必须remove()
// - 监听器、回调要及时注销
}
}
/**
* 判断对象可回收总结表
*
* 方法 优点 缺点 适用场景
* 引用计数 实时性高,无需STW 循环引用无法处理 Python、PHP早期版本
* 可达性分析 精准,无循环引用问题 需要STW 主流JVM
*
* GC Roots类型:
* 栈引用、静态引用、常量引用、JNI引用、同步锁、JVM内部引用
*
* 判定流程:
* 第一次标记(不可达)→ 筛选(是否需要finalize)→ 第二次标记(finalize后是否可达)→ 回收
*/
// 面试金句
// "判断对象是否可回收就像'寻找孤儿':
// 引用计数法是'数孩子',每个对象记录被引用的次数,但无法处理'孩子互相指着'的孤儿院(循环引用);
// 可达性分析是'找祖宗',从GC Roots这个'老祖宗'出发,能找到的都是'家族成员',找不到的就是'孤儿'。
// 现代JVM通过三色标记算法,在找孤儿的过程中还能处理'新出生的婴儿'(并发标记),
// 既保证了准确性,又减少了'打扰大家的时间'(STW)。
// 我曾在项目中通过MAT分析GC Roots,发现一个静态Map持有大量对象导致内存泄漏,
// 改为WeakHashMap后问题解决。理解GC Roots,才能真正掌握内存管理。"
|
常见的 GC 算法有哪些?各自的优缺点?
标记-清除算法(Mark-Sweep)
一句话原理:标记-清除算法分为两阶段:先从GC Roots出发标记所有存活对象,然后清除未标记对象,但会产生内存碎片,导致大对象无法分配而触发提前GC。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // 标记-清除算法伪代码
class MarkSweepGC {
// 标记阶段
void markPhase() {
for (Root root : gcRoots) {
markObject(root.getObject()); // 递归标记可达对象
}
}
void markObject(Object obj) {
if (!obj.isMarked()) {
obj.setMarked(true); // 设置标记位
for (Object ref : obj.getReferences()) {
markObject(ref); // 递归标记引用对象
}
}
}
// 清除阶段
void sweepPhase() {
for (Object obj : heap) {
if (!obj.isMarked()) {
free(obj); // 回收未标记对象
} else {
obj.setMarked(false); // 清除标记,准备下次GC
}
}
}
}
// CMS收集器中的标记-清除实现
class CMSCollector {
void cmsCollect() {
// 初始标记(STW)
initialMark();
// 并发标记
concurrentMark();
// 重新标记(STW)
remark();
// 并发清除
concurrentSweep(); // 标记-清除算法
}
}
|
项目场景:在老年代CMS收集器中采用标记-清除算法,虽然避免了长时间STW,但运行一段时间后内存碎片严重,导致大对象分配失败而触发Full GC。通过添加JVM参数-XX:+UseCMSCompactAtFullCollection在Full GC时进行碎片整理,解决了这个问题。
标记-复制算法(Mark-Copy)
一句话原理:标记-复制将内存分为两块,只使用其中一块,GC时将存活对象复制到另一块,然后清空当前块,实现内存紧凑无碎片,但可用内存减半,适用于朝生夕死的新生代。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| // 标记-复制算法伪代码
class CopyingGC {
private MemoryArea from = new MemoryArea(size); // 活动区
private MemoryArea to = new MemoryArea(size); // 空闲区
void collect() {
// 遍历GC Roots,复制存活对象到to区
for (Root root : gcRoots) {
Object obj = root.getObject();
if (obj != null) {
Object newObj = copyObject(obj, to); // 复制
root.updateReference(newObj); // 更新引用
}
}
// 清空from区
from.clear();
// 交换from和to
swap();
}
Object copyObject(Object obj, MemoryArea to) {
if (obj.isForwarded()) {
return obj.getForwardingAddress(); // 已被复制,返回新地址
}
// 复制对象到to区
Object newObj = to.allocate(obj.getSize());
obj.copyTo(newObj);
obj.setForwardingAddress(newObj); // 设置转发指针
// 复制引用对象
for (Object ref : obj.getReferences()) {
Object newRef = copyObject(ref, to);
newObj.updateReference(ref, newRef);
}
return newObj;
}
}
// HotSpot新生代Serial收集器实现
class DefNewGeneration {
void collect() {
// Eden和From存活对象复制到To
copyLiveObjects();
// 交换From和To
swapSpaces();
// Eden清空
eden.clear();
}
}
// 默认比例:Eden : From : To = 8 : 1 : 1
// -XX:SurvivorRatio=8
|
项目场景:在用户请求处理系统中,大量请求对象在新生代快速分配和回收,标记-复制算法让每次Minor GC只复制少量存活对象(约10%),吞吐量极高。通过调整Survivor空间比例,避免了对象过早晋升老年代。
标记-整理算法(Mark-Compact)
一句话原理:标记-整理算法结合了前两者的优点:先标记存活对象,然后将存活对象向一端移动(整理),使内存连续无碎片,但移动对象需要更新所有引用,导致STW较长,适用于对象存活率高的老年代。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| // 标记-整理算法伪代码
class MarkCompactGC {
void collect() {
// 1. 标记阶段
markPhase(); // 同标记-清除
// 2. 计算新地址
computeForwardingAddress();
// 3. 更新引用
updateReferences();
// 4. 移动对象
compactHeap();
}
void computeForwardingAddress() {
// 计算每个存活对象在整理后的新地址
Address newStart = heapStart;
for (Object obj : liveObjects) {
obj.setForwardingAddress(newStart);
newStart += obj.getSize();
}
}
void updateReferences() {
// 遍历所有对象,将引用更新为新地址
for (Object obj : heap) {
if (obj.isLive()) {
for (Reference ref : obj.getReferences()) {
if (ref.getObject() != null) {
ref.update(ref.getObject().getForwardingAddress());
}
}
}
}
// 更新GC Roots引用
for (Root root : gcRoots) {
root.update(root.getObject().getForwardingAddress());
}
}
void compactHeap() {
// 将对象移动到新地址
for (Object obj : liveObjects) {
copyObject(obj, obj.getForwardingAddress());
}
}
}
// 老年代Serial Old收集器实现
class TenuredGeneration {
void collect() {
// 标记-整理算法实现
markLiveObjects();
compactHeap(); // 移动对象,消除碎片
}
}
|
项目场景:在交易系统的老年代中,存活对象多且需要长期保存,标记-整理算法避免了复制算法的空间浪费,同时消除了标记-清除的碎片问题。虽然每次Full GC耗时较长,但频率低(几天一次),对业务影响可控。
分代收集算法(Generational Collection)
一句话原理:分代收集根据对象生命周期将堆分为新生代(标记-复制)和老年代(标记-整理/清除),不同区域使用不同算法,实现高吞吐和低停顿的平衡,是当前主流JVM的GC策略。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // HotSpot分代收集框架
class GenCollectedHeap {
Generation youngGen; // 新生代:标记-复制
Generation oldGen; // 老年代:标记-整理/清除
void collect(GCType type) {
if (type == YOUNG_GC) {
youngGen.collect(); // Minor GC,快速
} else {
// Full GC,先尝试老年代收集
oldGen.collect();
if (needMoreSpace()) {
// 老年代收集后仍不足,执行Full GC
fullCollect();
}
}
}
}
// 新生代收集器选择
class YoungGeneration {
void collect() {
if (useSerialGC) {
new DefNewGeneration().collect(); // Serial
} else if (useParallelGC) {
new ParallelScavenge().collect(); // Parallel
} else if (useG1GC) {
new G1YoungCollector().collect(); // G1
}
}
}
// 老年代收集器选择
class OldGeneration {
void collect() {
if (useSerialGC) {
new TenuredGeneration().collect(); // Serial Old
} else if (useParallelGC) {
new ParallelCompact().collect(); // Parallel Old
} else if (useCMS) {
new CMSCollector().collect(); // CMS
} else if (useG1GC) {
new G1MixedCollector().collect(); // G1混合回收
}
}
}
|
项目场景:在大型电商系统中,采用G1垃圾收集器(分代收集+分区管理),新生代对象快速复制回收,老年代通过混合回收逐步整理。通过-XX:MaxGCPauseMillis=200设置目标停顿时间,G1自动调整新生代大小和回收策略,在保证吞吐量的同时将GC停顿控制在200ms内。
完整对比与实战选型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
| /**
* GC算法完整对比指南
*/
public class GCAlgorithmGuide {
/**
* 1. 算法对比表
*/
public class AlgorithmComparison {
// 算法 优点 缺点 适用场景
// 标记-清除 实现简单,不移动对象 内存碎片,分配效率低 CMS老年代
// 标记-复制 无碎片,分配高效 内存浪费一半 新生代
// 标记-整理 无碎片,空间利用率高 移动对象,STW长 老年代
// 分代收集 综合优势 实现复杂 主流JVM
}
/**
* 2. 内存分配效率分析
*/
public class AllocationEfficiency {
// 标记-复制:指针碰撞(高效)
// TLAB + 指针碰撞,分配只需10-20ns
// 标记-清除:空闲列表(低效)
// 需要遍历空闲列表查找合适空间,分配可能>100ns
// 标记-整理:指针碰撞(高效,但整理后)
// 整理后同复制算法,但整理过程耗时
}
/**
* 3. 碎片问题量化
*/
public class FragmentationAnalysis {
// 标记-清除运行100次后,碎片率可达30-50%
// 可能导致提前Full GC
// 标记-复制和标记-整理无碎片
// 碎片影响
// 1. 大对象分配失败
// 2. GC频率增加
// 3. CPU使用率升高
}
/**
* 4. 实战:GC算法调优案例
*/
public class GCTuningCases {
// 案例1:批处理系统 - 吞吐量优先
// 使用Parallel Scavenge + Parallel Old
// -XX:+UseParallelGC -XX:ParallelGCThreads=8
// 目标:最大化吞吐量,GC时间占比控制在1%以内
// 案例2:Web服务 - 响应时间优先
// 使用G1GC
// -XX:+UseG1GC -XX:MaxGCPauseMillis=200
// 目标:STW控制在200ms内
// 案例3:大内存系统(>32G)
// 使用ZGC(JDK11+)
// -XX:+UseZGC -Xmx64g
// 目标:STW<10ms,支持TB级堆
// 案例4:老年代碎片问题
// CMS + 碎片整理
// -XX:+UseConcMarkSweepGC
// -XX:+UseCMSCompactAtFullCollection
// -XX:CMSFullGCsBeforeCompaction=5
}
/**
* 5. 算法实现源码分析
*/
public class SourceCodeAnalysis {
// Serial收集器(标记-复制)
// defGeneration.cpp - defNewGeneration::collect()
// Parallel Scavenge(标记-复制)
// psScavenge.cpp - PSScavenge::invoke()
// CMS(标记-清除)
// concurrentMarkSweepGeneration.cpp - CMSCollector::collect_in_background()
// G1(分区+标记-复制)
// g1CollectedHeap.cpp - G1CollectedHeap::do_collection()
// ZGC(染色指针+读屏障)
// zCollectedHeap.cpp - ZCollectedHeap::collect()
}
/**
* 6. 选型决策树
*/
public class SelectionTree {
// 1. 堆大小?
// ├─ < 4G → Serial/Parallel
// └─ 4-32G → G1
// └─ 响应时间敏感? → G1 with pause target
// └─ >32G → ZGC/Shenandoah
//
// 2. 应用类型?
// ├─ 批处理/后台 → 吞吐量优先 (Parallel)
// └─ Web/实时 → 低延迟优先 (G1/ZGC)
//
// 3. CPU核心数?
// ├─ 少核心 → Serial(避免竞争)
// └─ 多核心 → Parallel/G1(利用多核)
}
}
/**
* GC算法总结表
*
* 算法 内存碎片 分配效率 GC效率 STW时间 适用代
* 标记-清除 高 低 中 短 老年代
* 标记-复制 无 高 高 中 新生代
* 标记-整理 无 中 低 长 老年代
* 分代收集 无 高 高 中 全堆
*
* JVM参数速查:
* -XX:+UseSerialGC: Serial + Serial Old
* -XX:+UseParallelGC: Parallel Scavenge + Parallel Old
* -XX:+UseConcMarkSweepGC: ParNew + CMS
* -XX:+UseG1GC: G1
* -XX:+UseZGC: ZGC
*/
// 面试金句
// "GC算法的演进反映了JVM对'吞吐量'和'停顿时间'的永恒追求:
// 标记-清除像'打扫房间不整理',虽然快但东西摆放杂乱(碎片);
// 标记-复制像'准备一个空房间,把需要的东西搬过去',整洁但浪费空间;
// 标记-整理像'把东西都推到墙边',整洁但搬运费时;
// 分代收集则像'不同房间不同打扫策略':玩具房(新生代)东西更新快,直接换新房间;
// 储藏室(老年代)东西固定,定期整理。
// 在项目中,我将批处理系统从CMS改为Parallel后,吞吐量提升30%;
// 将Web服务从Parallel改为G1后,TP99从500ms降到200ms。
// 理解算法本质,才能做出正确的GC选型。"
|
JVM 有哪些垃圾回收器?G1 和 CMS 的区别?
常见垃圾回收器概览
一句话原理:JVM提供七种垃圾回收器,按代际分为新生代(Serial、ParNew、Parallel Scavenge)、老年代(Serial Old、Parallel Old、CMS)和全堆(G1),以及JDK11+的低延迟回收器(ZGC、Shenandoah)。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // HotSpot中垃圾回收器家族
class CollectedHeap {
// 新生代回收器
// - Serial: DefNewGeneration (单线程,复制算法)
// - ParNew: ParNewGeneration (多线程,复制算法)
// - Parallel Scavenge: PSYoungGen (吞吐量优先,复制算法)
// 老年代回收器
// - Serial Old: TenuredGeneration (单线程,标记-整理)
// - Parallel Old: PSOldGen (多线程,标记-整理)
// - CMS: ConcurrentMarkSweepGeneration (并发,标记-清除)
// 全堆回收器
// - G1: G1CollectedHeap (分区,复制+整理)
// - ZGC: ZCollectedHeap (染色指针,并发)
}
// 通过命令行参数选择回收器
public class GCSelector {
// -XX:+UseSerialGC Serial + Serial Old
// -XX:+UseParNewGC ParNew + CMS
// -XX:+UseParallelGC Parallel Scavenge + Parallel Old
// -XX:+UseConcMarkSweepGC ParNew + CMS
// -XX:+UseG1GC G1
// -XX:+UseZGC ZGC (JDK11+)
}
// 查看当前使用的GC
java -XX:+PrintCommandLineFlags -version
|
项目场景:在微服务架构中,不同服务根据业务特点选择不同GC:批处理服务用Parallel追求吞吐量,实时交易用G1控制延迟,大内存数据分析用ZGC支撑百GB堆。通过监控GC日志,持续优化回收器参数。
CMS回收器深度解析
一句话原理:CMS(Concurrent Mark Sweep)是以获取最短回收停顿时间为目标的老年代回收器,基于标记-清除算法,通过并发执行大部分阶段减少STW,但存在内存碎片和CPU敏感问题。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // CMS回收流程源码级分析
class CMSCollector {
void collect() {
// 1. 初始标记(STW)—— 标记GC Roots直接关联对象
// 停顿时间很短
initialMark();
// 2. 并发标记 —— 和应用线程同时运行
// 遍历对象图,标记所有可达对象
concurrentMark();
// 3. 重新标记(STW)—— 修正并发期间变动的引用
// 停顿时间比初始标记稍长
remark();
// 4. 并发清除 —— 和应用线程同时运行
// 回收未标记对象
concurrentSweep();
// 5. 重置(可选)
reset();
}
// 并发失败处理
void handleConcurrentModeFailure() {
// 如果并发收集时老年代空间不足
// 退化为Serial Old进行Full GC(单线程标记-整理)
serialOldCollect();
}
}
// CMS日志解读
// [GC (CMS Initial Mark) [1 CMS-initial-mark: 5120K(8192K)] 6128K(16384K), 0.0012345 secs]
// [CMS-concurrent-mark: 0.123/0.456 secs]
// [CMS-concurrent-preclean: 0.012/0.023 secs]
// [GC (CMS Final Remark) [YG occupancy: 1234 K (8192 K)] [Rescan (parallel) , 0.023456 secs]
// [CMS-concurrent-sweep: 0.234/0.567 secs]
|
项目场景:在门户网站的内容管理系统中,用户请求对响应时间敏感,采用ParNew+CMS组合。通过调优并发线程数和晋升阈值,将Full GC频率控制在几天一次,每次STW不超过100ms,保证了用户体验。
G1回收器深度解析
一句话原理:G1(Garbage First)是服务端模式的垃圾回收器,将堆划分为2048个Region,通过预测模型优先回收垃圾最多的Region,实现可预测的停顿时间,是CMS的替代方案。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| // G1回收流程源码级分析
class G1CollectedHeap {
// 堆划分为多个Region(1-32MB)
static final int MAX_REGIONS = 2048;
HeapRegion[] _regions;
void collect() {
// G1有两种回收模式
// 1. Young GC —— 新生代回收
// 复制存活对象到Survivor或晋升老年代
youngGC();
// 2. Mixed GC —— 混合回收(当老年代到达阈值)
// 同时回收新生代和部分老年代Region
if (needMixedGC()) {
mixedGC();
}
}
// 混合回收流程
void mixedGC() {
// 第一阶段:全局并发标记
// 1.1 初始标记(STW):标记GC Roots
initialMark();
// 1.2 并发标记:遍历对象图
concurrentMark();
// 1.3 最终标记(STW):处理SATB缓冲区
remark();
// 1.4 清理(STW):统计存活对象
cleanup();
// 第二阶段:拷贝存活对象
// 选择垃圾最多的Region进行回收
selectCSet(); // 根据垃圾比例选择Region集合
evacuateCSet(); // 复制存活对象
}
// 停顿时间预测模型
double predictGCPauseTime(CollectionSet cset) {
// 根据历史数据预测回收这些Region需要的时间
double time = 0;
for (Region r : cset) {
time += predictRegionTime(r);
}
return time;
}
}
// G1日志解读
// [GC pause (G1 Evacuation Pause) (young) 256M->64M(1024M), 0.1234567 secs]
// [GC pause (Mixed) (young) (initial-mark) 512M->128M(1024M), 0.2345678 secs]
// [GC concurrent-root-region-scan-start]
// [GC concurrent-mark-start]
// [GC concurrent-mark-end, 0.3456789 secs]
// [GC remark, 0.0123456 secs]
// [GC cleanup, 0.0012345 secs]
|
项目场景:在金融交易系统中,要求STW时间严格控制在200ms内。使用G1并设置-XX:MaxGCPauseMillis=200,G1通过动态调整新生代大小和回收Region数量,在高峰期也能满足停顿时间要求,且避免了CMS的碎片问题。
G1 vs CMS 核心区别
一句话原理:CMS是传统分代收集器,标记-清除导致碎片;G1是分区式收集器,复制+整理无碎片,且提供可预测停顿。CMS适合重视响应时间但对停顿无严格要求的系统,G1适合大堆+严格停顿要求的系统。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| /**
* G1 vs CMS 详细对比
*/
public class CMSvsG1 {
// 1. 堆结构对比
// CMS: 连续的新生代(Eden+Survivor) + 连续的老年代
// G1: 2048个Region,每个可属于Eden/Survivor/Old/Humongous
// 2. 回收算法对比
// CMS: 新生代标记-复制,老年代标记-清除
// G1: 新生代标记-复制,老年代标记-复制(整理)
// 3. 碎片问题
// CMS: 老年代产生碎片,可能导致Full GC
// G1: 复制算法无碎片
// 4. 停顿预测
// CMS: 无法预测,可能突发Full GC
// G1: 基于预测模型,可控停顿
// 5. CPU消耗
// CMS: 并发标记消耗CPU
// G1: 并发标记+复制,CPU消耗略高
// 6. 内存占用
// CMS: 需要预留空间(浮动垃圾)
// G1: 需要Remembered Set维护跨代引用(额外内存5-10%)
}
// 源码中的关键区别
class ConcurrentMarkSweepGeneration {
// CMS老年代
// 使用空闲列表管理内存(碎片)
CompactibleFreeListSpace* _cmsSpace;
}
class G1CollectedHeap {
// G1堆
// 通过TAMS指针并发标记,使用RSet记录跨Region引用
HeapRegion* _hrm;
G1RegionToSpaceMapper* _hrm;
}
// 选择建议
public class GCSelection {
// 选CMS的情况:
// - 堆大小 < 8G
// - 响应时间敏感但无严格停顿要求
// - CPU资源充足
// -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
// 选G1的情况:
// - 堆大小 > 8G
// - 要求可预测停顿(MaxGCPauseMillis)
// - 希望避免碎片问题
// -XX:+UseG1GC -XX:MaxGCPauseMillis=200
// 选ZGC的情况(JDK11+):
// - 超大堆 > 100G
// - 极低停顿 < 10ms
// -XX:+UseZGC -Xmx100g
}
|
完整实战指南
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
| /**
* 垃圾回收器实战选型指南
*/
public class GCPracticeGuide {
/**
* 1. 各回收器适用场景
*/
public class GCScenarios {
// Serial: 客户端应用,单核环境,堆 < 100M
// 参数: -XX:+UseSerialGC
// Parallel: 批处理,数据分析,后台任务
// 参数: -XX:+UseParallelGC -XX:ParallelGCThreads=8
// 目标: 最大化吞吐量
// CMS: Web应用,中等堆,响应时间敏感但无严格约束
// 参数: -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70
// G1: 大内存服务,要求可控停顿时间
// 参数: -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=16m
// ZGC: 超大堆,极低延迟要求
// 参数: -XX:+UseZGC -Xmx64g -XX:ZCollectionInterval=60
}
/**
* 2. CMS调优实战
*/
public class CMSTuning {
// 常见参数
// -XX:CMSInitiatingOccupancyFraction=70 老年代70%时开始并发收集
// -XX:+UseCMSInitiatingOccupancyOnly 只使用此阈值
// -XX:+CMSScavengeBeforeRemark 重新标记前执行Minor GC
// 并发失败处理
// -XX:+UseCMSCompactAtFullCollection Full GC时整理碎片
// -XX:CMSFullGCsBeforeCompaction=5 每5次Full GC整理一次
// 调优案例
public void cmsTuningCase() {
// 问题:频繁并发失败,退化为Full GC
// 分析:老年代碎片太多,无法分配对象
// 解决:开启碎片整理,降低触发阈值
// -XX:CMSInitiatingOccupancyFraction=60
// -XX:+UseCMSCompactAtFullCollection
// -XX:CMSFullGCsBeforeCompaction=2
}
}
/**
* 3. G1调优实战
*/
public class G1Tuning {
// 核心参数
// -XX:MaxGCPauseMillis=200 目标停顿时间
// -XX:G1HeapRegionSize=16m Region大小(1-32MB)
// -XX:G1NewSizePercent=5 新生代初始比例
// -XX:G1MaxNewSizePercent=60 新生代最大比例
// 调优案例1:停顿时间超预期
public void pauseTimeTuning() {
// 问题:实际停顿 > 目标200ms
// 分析:Humongous对象分配过多(超过Region 50%)
// 解决:调大RegionSize,优化大对象
// -XX:G1HeapRegionSize=32m
}
// 调优案例2:Mixed GC频繁
public void mixedGCTuning() {
// 问题:Mixed GC频繁,影响吞吐量
// 分析:IHOP(InitiatingHeapOccupancyPercent)过低
// 解决:提高触发阈值
// -XX:InitiatingHeapOccupancyPercent=45 → 55
}
// G1最佳实践
// 1. 设置合理的停顿时间(200-500ms)
// 2. 监控Evacuation Failure
// 3. 避免过多Humongous对象
}
/**
* 4. GC日志分析实战
*/
public class GCLogAnalysis {
// 必备参数
// -Xloggc:/path/to/gc.log
// -XX:+PrintGCDetails
// -XX:+PrintGCDateStamps
// -XX:+PrintTenuringDistribution
// -XX:+PrintHeapAtGC
// CMS日志分析
// 关注点:
// - 并发标记耗时
// - 重新标记STW时长
// - 并发失败频率
// - 碎片程度
// G1日志分析
// 关注点:
// - Young GC耗时
// - Mixed GC频率
// - 停顿时间是否符合预期
// - Humongous分配情况
public void analyzeGCLog() {
// 使用GCViewer或在线工具分析
// 1. 平均停顿时间
// 2. 吞吐量(应用时间/总时间)
// 3. GC频率
// 4. 各阶段耗时分布
}
}
/**
* 5. 监控与告警
*/
public class GCMonitoring {
public void monitorGC() {
// JMX监控
List<GarbageCollectorMXBean> gcBeans =
ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean gc : gcBeans) {
long count = gc.getCollectionCount();
long time = gc.getCollectionTime();
String name = gc.getName();
System.out.println(name + ": count=" + count + ", time=" + time);
}
// 告警阈值
// - Full GC次数 > 1次/小时
// - GC总时间 > 5秒/分钟
// - 连续多次GC
}
}
}
/**
* 垃圾回收器总结表
*
* 回收器 适用范围 算法 STW 特点
* Serial 新生代 复制 短 单线程,客户端
* ParNew 新生代 复制 短 多线程,配合CMS
* Parallel 新生代 复制 短 吞吐量优先
* Serial Old 老年代 标记-整理 长 单线程,CMS后备
* Parallel Old 老年代 标记-整理 长 吞吐量优先
* CMS 老年代 标记-清除 很短 并发,响应时间优先
* G1 全堆 复制+整理 可控 大堆,可预测停顿
* ZGC 全堆 染色指针 极短 <10ms,超大堆
*
* 选择建议:
* 4G以下堆 + 单核:Serial
* 4-8G堆 + 吞吐量优先:Parallel
* 4-8G堆 + 响应时间优先:CMS
* 8-32G堆 + 可控停顿:G1
* 32G以上堆 + 极低延迟:ZGC
*/
// 面试金句
// "CMS和G1就像'老牌工匠'和'智能机器人'的区别:
// CMS是经验丰富的老师傅(分代清楚),但干完活总留点碎屑(内存碎片);
// G1是现代化的机器人,把工作区域分成小格子(Region),
// 哪块垃圾最多先去哪(Garbage First),还能精确预估完工时间(停顿预测)。
// 在迁移到G1的项目中,我们把CMS的碎片问题和突发Full GC都解决了,
// 通过设置MaxGCPauseMillis,让GC变得可预期,就像给系统装上了'定时器'。
// 理解不同回收器的设计哲学,才能为应用选择最合适的GC。"
|
G1 是如何实现“可预测停顿”的?
核心原理:Region化+停顿预测模型
一句话原理:G1通过将堆划分为2048个独立Region,基于历史数据建立预测模型,在每次回收时选择垃圾最多的Region集合(CSet)并计算预估停顿时间,确保总时间不超过设定的目标(-XX:MaxGCPauseMillis),实现可预测的停顿。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // G1核心数据结构 - Region划分
class G1CollectedHeap {
// 堆划分为2048个Region(默认)
static const uint MAX_REGIONS = 2048;
HeapRegion* _regions; // Region数组
// 每个Region有独立的状态
class HeapRegion {
RegionType _type; // Eden/Survivor/Old/Humongous
size_t _used; // 已使用空间
size_t _live_bytes; // 存活对象字节数
size_t _garbage_bytes; // 垃圾对象字节数
volatile size_t _gc_efficiency; // 回收效率(垃圾量/回收时间)
}
}
// 停顿预测模型核心实现
class G1Analytics {
// 历史数据衰减因子(默认0.5)
double _sigma;
// 各种操作的预测模型
TruncatedSeq* _rs_length_seq; // Remembered Set扫描时间预测
TruncatedSeq* _cost_per_byte_seq; // 拷贝每字节成本预测
TruncatedSeq* _young_length_seq; // 新生代Region数量预测
// 预测单个Region回收时间
double predict_region_time(HeapRegion* hr) {
double rs_length_time = predict_rs_length_time(hr);
double copy_time = predict_copy_time(hr);
return rs_length_time + copy_time;
}
// 使用历史数据加权平均预测
double predict(TruncatedSeq* seq) {
double avg = seq->avg(); // 平均值
double sd = seq->sd(); // 标准差
return avg + _sigma * sd; // 加权预测(包含波动)
}
}
|
项目场景:在金融交易系统中,要求GC停顿严格控制在200ms内。G1通过预测模型分析历史数据,在Mixed GC时只选择回收时间总和小于200ms的Region集合,即使堆中有大量垃圾也分批回收,确保每次停顿都在目标范围内。
停顿预测模型工作机制
一句话原理:G1维护每个Region的回收效率(垃圾量/回收时间)和历史耗时数据,通过衰减移动平均算法动态调整预测值,在每次GC前计算选定Region的预估总耗时,动态调整新生代大小和回收Region数量。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| // G1停顿预测详细实现
class G1CollectionSetChooser {
G1CollectedHeap* _g1h;
G1Analytics* _analytics;
// 构建回收集合(CSet)
CollectionSet* select_collection_set(double target_pause_time) {
CollectionSet* cset = new CollectionSet();
double predicted_time = 0;
// 1. 先添加所有新生代Region(必须回收)
predicted_time += add_young_regions(cset);
// 2. 如果还有剩余时间,添加老年代Region
if (predicted_time < target_pause_time) {
// 按回收效率排序老年代Region(垃圾量/时间)
HeapRegion** sorted_regions = sort_old_regions_by_efficiency();
for (HeapRegion* hr : sorted_regions) {
double region_time = predict_region_time(hr);
if (predicted_time + region_time <= target_pause_time) {
cset->add(hr);
predicted_time += region_time;
} else {
break; // 时间不够了,停止添加
}
}
}
// 记录预测结果
_g1h->set_predicted_pause_time(predicted_time);
return cset;
}
// 预测单个Region回收时间
double predict_region_time(HeapRegion* hr) {
// 1. 扫描RSet(Remembered Set)的时间
size_t rs_length = hr->rem_set()->occupied();
double rs_scan_time = _analytics->predict_rs_scan_time(rs_length);
// 2. 拷贝存活对象的时间
size_t live_bytes = hr->live_bytes();
double copy_time = _analytics->predict_copy_time(live_bytes);
// 3. 其他固定开销(更新引用等)
double fixed_time = _analytics->predict_fixed_overhead();
return rs_scan_time + copy_time + fixed_time;
}
// 衰减平均算法
double calculate_decayed_average(TruncatedSeq* seq, double new_value) {
// 使用衰减因子α(默认0.5)加权
double old_avg = seq->avg();
double alpha = 0.5; // 衰减因子
double new_avg = alpha * new_value + (1 - alpha) * old_avg;
seq->add(new_avg);
return new_avg;
}
}
|
项目场景:在电商秒杀系统中,流量波动极大。G1的预测模型能快速适应变化:高峰期自动减少每次回收的Region数量保证停顿时间,低谷期多回收一些提高吞吐量。通过-XX:+PrintAdaptiveSizePolicy查看动态调整过程,验证了模型的准确性。
并发标记与RSet优化
一句话原理:G1通过并发标记收集Region存活信息,利用Remembered Set(RSet)记录跨Region引用,在回收时精确知道需要扫描的Region,避免全堆扫描,大幅提升预测准确性和回收效率。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| // RSet(Remembered Set)实现
class HeapRegion {
PerRegionTable* _rem_set; // 指向本Region的引用记录
// RSet记录了其他Region中谁引用了本Region的对象
// 格式:其他Region的索引 + 引用卡表
}
class G1RemSet {
// 扫描RSet,只处理真正有引用的Region
void scan_rem_set(CollectionSet* cset) {
for (HeapRegion* hr : cset) {
PerRegionTable* prt = hr->rem_set();
// 遍历所有引用本Region的外部Region
for (RegionIdx other : prt->referencing_regions()) {
// 只扫描cset中Region的RSet
if (cset->contains(other)) {
scan_cards(prt->get_cards_for(other));
}
}
}
}
}
// 并发标记收集存活信息
class G1ConcurrentMark {
// 标记每个Region的存活对象
void mark_live_objects() {
G1CMBitMap* _bm; // 位图标记存活对象
// 并发标记阶段
for (HeapRegion* hr : _g1h->regions()) {
size_t live = count_live_in_region(hr);
hr->set_live_bytes(live); // 更新存活数据
hr->set_garbage_bytes(hr->used() - live); // 计算垃圾量
}
}
// 计算回收效率(垃圾量/预估时间)
double calculate_gc_efficiency(HeapRegion* hr) {
size_t garbage = hr->garbage_bytes();
double time = predict_reclaim_time(hr);
return (double)garbage / time; // 每毫秒能回收多少字节
}
}
// 最终CSet选择
class G1Policy {
void choose_collection_set(double target_pause) {
// 1. 按回收效率排序所有老年代Region
// 2. 从高到低选择直到达到目标时间
// 3. 记录本次选择的Region供下次参考
}
}
|
项目场景:在微服务架构中,服务间调用频繁,对象引用关系复杂。G1的RSet机制精确记录了跨Region引用,回收时只扫描必要的部分,避免了全堆扫描带来的长时间STW,确保即使在复杂引用关系下停顿时间依然可控。
完整实战与调优指南
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
| /**
* G1可预测停顿完整实战指南
*/
public class G1PauseTimeGuide {
/**
* 1. 核心参数详解
*/
public class G1Parameters {
// -XX:MaxGCPauseMillis=200 目标停顿时间(默认200ms)
// -XX:G1HeapRegionSize=16m Region大小(1-32MB)
// -XX:G1NewSizePercent=5 新生代最小占比
// -XX:G1MaxNewSizePercent=60 新生代最大占比
// -XX:G1MixedGCLiveThresholdPercent=85 混合回收存活阈值
// -XX:+UnlockExperimentalVMOptions -XX:G1MixedGCLiveThresholdPercent=85
// 停顿时间设置原则
// - 太短(<50ms):每次回收极少Region,垃圾堆积
// - 太长(>500ms):失去低延迟意义
// - 适中(200ms):平衡吞吐量和延迟
}
/**
* 2. 停顿预测监控
*/
public class PauseTimeMonitoring {
public void monitorPrediction() {
// 开启预测日志
// -XX:+PrintAdaptiveSizePolicy
// -XX:+PrintGCDetails
// 日志示例:
// [G1Ergonomics (Mixed GCs) continue mixed GCs, reason: candidate old regions available,
// candidate old regions: 124, reclaimable: 4278190080, threshold: 3070230528]
// [G1Ergonomics (CSet Construction) start choosing CSet, predicted base time: 10.00ms,
// remaining time: 190.00ms, target pause time: 200.00ms]
// [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 48 regions,
// survivors: 8 regions, predicted young region time: 80.00ms]
// [G1Ergonomics (CSet Construction) add old regions to CSet, added: 12 regions,
// predicted time: 115.00ms, remaining time: 75.00ms]
}
// 分析预测准确性
public void checkPredictionAccuracy() {
// 比较预测时间和实际时间
// 实际时间 = [Times: user=0.20 sys=0.01, real=0.21 secs]
// 预测时间 = predicted pause time: 200ms
// 误差率 = |实际-预测|/预测
// 误差>20%说明预测模型需要调整
}
}
/**
* 3. 调优实战案例
*/
public class G1TuningCases {
// 案例1:停顿时间超出目标
// 现象:实际停顿250ms > 目标200ms
// 分析:Humongous对象过多,拷贝耗时超预期
// 解决:-XX:G1HeapRegionSize=32m 调大Region,减少大对象跨Region
// 案例2:GC频率过高
// 现象:GC频繁,但每次停顿很小
// 分析:新生代太小,对象频繁晋升
// 解决:-XX:G1NewSizePercent=10 增大新生代
// 案例3:Mixed GC停顿波动大
// 现象:有时120ms,有时280ms
// 分析:RSet扫描时间预测不准
// 解决:-XX:G1ConfidencePercent=50 提高预测置信度
// 案例4:Full GC发生
// 现象:出现串行Full GC
// 分析:并发标记赶不上对象分配速度
// 解决:-XX:InitiatingHeapOccupancyPercent=35 提早触发并发标记
}
/**
* 4. 预测模型调整参数
*/
public class PredictionTuning {
// -XX:G1ConfidencePercent=50 预测置信度(默认50)
// 值越大,预留更多时间余量,停顿更可控但GC效率降低
// -XX:G1MixedGCLiveThresholdPercent=85 混合回收存活阈值
// 存活对象超过85%的Region不回收(回收效率低)
// -XX:G1OldCSetRegionThresholdPercent=5 每次Mixed GC最大老年代Region比例
// 调优原则:
// 1. 先设置合理的停顿目标
// 2. 观察预测准确性
// 3. 调整置信度平衡效率和控制
// 4. 监控Humongous分配
}
/**
* 5. 实战:从CMS迁移到G1
*/
public class MigrateFromCMSToG1 {
public void migrationSteps() {
// 1. 替换启动参数
// 原:-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
// 新:-XX:+UseG1GC -XX:MaxGCPauseMillis=200
// 2. 监控指标对比
// CMS指标:Full GC频率、碎片率
// G1指标:停顿时间达标率、Mixed GC效率
// 3. 逐步优化
// 初始:目标200ms,保持默认
// 调整:根据日志优化Region大小、新生代比例
// 稳定:监控1-2周,微调参数
// 迁移效果:
// - CMS时:每周2次Full GC,每次3-5秒
// - G1后:无Full GC,Mixed GC每次180ms
}
}
/**
* 6. 问题排查流程
*/
public class ProblemDiagnosis {
public void diagnosePauseTimeIssue() {
// 1. 确认实际停顿是否达标
// grep "real=" gc.log | awk -F'real=' '{print $2}' | sort -n
// 2. 查看预测值与实际值
// grep "predicted pause time" gc.log
// 3. 分析各阶段耗时
// - Evacuation Pause: 对象拷贝
// - Root Scanning: GC Roots扫描
// - RSet Scanning: Remembered Set扫描
// 4. 检查Humongous分配
// grep "Humongous" gc.log
// 5. 调整预测置信度或Region大小
}
}
}
/**
* G1可预测停顿总结表
*
* 机制 作用 关键参数
* Region划分 细化回收粒度 G1HeapRegionSize
* 预测模型 基于历史预估回收时间 G1ConfidencePercent
* CSet选择 选择回收效率高的Region MaxGCPauseMillis
* RSet 避免全堆扫描 自动
* 并发标记 收集存活信息 InitiatingHeapOccupancyPercent
*
* 预测公式:
* 总停顿时间 = 固定开销 + 新生代时间 + 选中的老年代Region时间
* 新生代时间 = Eden拷贝时间 + Survivor拷贝时间 + RSet扫描时间
* Region时间 = RSet扫描时间 + 存活对象拷贝时间 + 引用更新时间
*
* 调优要点:
* 1. 合理设置停顿目标(200-500ms)
* 2. 监控预测准确性,调整置信度
* 3. 避免过多Humongous对象
* 4. 观察Mixed GC的Region选择
*/
// 面试金句
// "G1的可预测停顿就像'智能预算系统':
// 首先把堆划分成2048个'小格子'(Region),每个格子都记录'历史清理成本'(预测数据);
// 每次GC前,就像做'项目预算',先算必须做的(新生代),再用剩余预算选择性价比最高的'格子'(老年代Region);
// 如果预算超了,就少选几个,保证不超支(停顿不超时)。
// 在金融交易系统中,我们通过设置200ms目标,G1即使在高峰期也能'精打细算',
// 既清理了垃圾,又不超过预算,彻底告别了CMS的'突发大额支出'(Full GC)。
// 理解这套机制,才能真正用好G1的'软实时'特性。"
|
你做过 GC 调优吗?用了哪些工具?关注哪些指标?
GC调优核心原理
一句话原理:GC调优的本质是通过调整堆内存大小、回收器选择、关键参数配置,在吞吐量(应用时间占比)、延迟(GC停顿时间)、内存占用三者之间找到业务场景的最佳平衡点。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // GC调优的三大目标(不可能三角)
public class GCTuningTriangle {
// 1. 吞吐量(Throughput):应用运行时间 / 总运行时间
// 目标:> 99% (GC时间 < 1%)
// 2. 延迟(Latency):GC停顿时间
// 目标:STW < 200ms,Full GC < 1s
// 3. 内存占用(Footprint):堆大小
// 目标:在满足前两者前提下尽量小
}
// 调优决策公式
// 吞吐量 = 应用时间 / (应用时间 + GC时间)
// GC频率 = 对象分配速率 / 每次GC回收量
// 停顿时间 = 存活对象数量 / 拷贝速度 + 固定开销
// JVM参数示例
// -Xms8g -Xmx8g # 堆内存
// -XX:+UseG1GC # 选择G1回收器
// -XX:MaxGCPauseMillis=200 # 目标停顿时间
// -XX:+PrintGCDetails # GC日志
|
项目场景:在一次双11大促前的压测中,发现系统TP99响应时间飙升至2秒,通过GC日志分析发现Full GC频繁。经过堆内存从4G调整到8G,并将CMS切换为G1,设置停顿目标200ms后,TP99降至300ms,顺利通过压测。
GC调优工具链
一句话原理:GC调优工具形成完整链条:jstat实时监控、GC日志离线分析、jmap堆dump、MAT/JProfiler内存分析、Arthas在线诊断,多维度定位问题根源。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| /**
* GC调优工具实战大全
*/
public class GCTuningTools {
// 1. jstat:实时监控GC
// jstat -gcutil <pid> 1000
// 输出:S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
// 监控指标:
// - YGC: Young GC次数
// - YGCT: Young GC总时间
// - FGC: Full GC次数
// - FGCT: Full GC总时间
// - GCT: 总GC时间
public void jstatExample() {
// 每秒打印GC信息
// jstat -gcutil 12345 1000
//
// S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
// 12.50 0.00 45.23 67.89 92.34 89.12 1234 23.45 5 3.21 26.66
//
// 解读:Eden区45%占用,老年代67%占用,Young GC 1234次,总时间23.45秒
}
// 2. GC日志分析
// 启用GC日志
// -Xloggc:/path/to/gc.log
// -XX:+PrintGCDetails
// -XX:+PrintGCDateStamps
// -XX:+PrintTenuringDistribution
// -XX:+PrintHeapAtGC
public void gcLogAnalysis() {
// 使用GCViewer或在线工具分析
// 关注指标:
// - 平均停顿时间
// - 最大停顿时间
// - GC频率
// - 吞吐量
// - 各代内存变化
}
// 3. jmap:堆dump
// jmap -dump:live,format=b,file=heap.hprof <pid>
// 4. MAT分析堆dump
// - Histogram:查看对象数量分布
// - Dominator Tree:查看大对象
// - Leak Suspects:自动泄漏检测
// - OQL:对象查询语言
// 5. Arthas在线诊断
// dashboard # 实时面板
// memory # 内存使用
// gc # GC详情
// jvm # JVM信息
// heapdump # 堆dump
// 6. VisualVM/JConsole
// - 实时监控CPU、内存、线程
// - 插件支持GC可视化
}
// 实战工具组合
public class ToolChain {
// 日常监控:jstat + GC日志
// 问题定位:Arthas + jmap + MAT
// 性能分析:VisualVM + JFR
// 长期趋势:Prometheus + Grafana
}
|
项目场景:在一次内存泄漏排查中,先用jstat发现老年代持续增长,然后用jmap dump堆内存,用MAT分析发现ThreadLocal未释放导致大量对象无法回收,最终通过Arthas在线验证修复效果,完整工具链帮助2小时内定位并解决问题。
核心监控指标
一句话原理:GC调优需要关注四大核心指标:吞吐量(GC时间占比)、停顿时间(STW时长)、GC频率(间隔时间)、内存变化(各代使用率),通过指标趋势判断GC健康状况。
一句话源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| /**
* GC核心监控指标详解
*/
public class GCMetrics {
// 1. 吞吐量指标
public class ThroughputMetrics {
// 计算公式:应用时间 / (应用时间 + GC时间)
// 目标:> 99% (GC时间 < 1%)
// 监控方式
// jstat -gcutil 查看GCT(总GC时间)
// 应用运行时间 - GCT = 应用时间
// 吞吐量 = (运行时间 - GCT) / 运行时间
}
// 2. 停顿时间指标
public class PauseTimeMetrics {
// Young GC: 期望 < 100ms
// Full GC: 期望 < 1s
// Max Pause: 最大停顿时间
// 监控方式
// GC日志中的 [Times: user=0.20 sys=0.01, real=0.21 secs]
// real字段就是实际停顿时间
}
// 3. GC频率指标
public class GCFrequencyMetrics {
// Young GC: 期望 10-30秒一次
// Full GC: 期望 几天一次
// 监控方式
// jstat -gcutil 中的YGC和FGC
// GC日志中统计次数
}
// 4. 内存变化指标
public class MemoryMetrics {
// Eden区使用率:GC后通常为0
// Survivor区使用率:应小于50%
// 老年代使用率:应缓慢增长
// 元空间使用率:应稳定
// 监控方式
// jstat -gcutil 中的E、O、M
// GC日志中的堆占用
}
// 5. 晋升指标
public class PromotionMetrics {
// 晋升大小:每次Young GC晋升到老年代的对象大小
// 晋升速率:单位时间晋升量
// 过大说明:Survivor空间不足或对象年龄设置不合理
}
// 6. 并发标记指标(G1/CMS)
public class ConcurrentMetrics {
// 并发标记耗时
// 重新标记STW时间
// Mixed GC频率
}
}
// 指标采集脚本
public class MetricsCollector {
public void collect() {
// jstat输出解析
// jstat -gcutil 12345 1000 > gc-metrics.csv
// 计算指标
// YGCT/YGC = 平均Young GC时间
// FGCT/FGC = 平均Full GC时间
// GCT/运行时间 = GC开销
// 告警阈值
// - Full GC次数 > 1次/小时
// - Young GC > 10秒/次
// - 吞吐量 < 99%
}
}
|
项目场景:在监控大盘中,我们设置了GC指标告警:Full GC次数>1次/小时触发警告,>5次/小时触发报警;Young GC平均时间>200ms触发优化;老年代使用率持续上升超过85%触发堆dump。这套指标帮助我们在问题发生前主动介入,将GC故障降低80%。
完整实战案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
| /**
* GC调优完整实战案例
*/
public class GCRealCase {
/**
* 案例1:CMS频繁Full GC调优
*/
public class CMSFullGCTuning {
// 现象:系统每天凌晨发生Full GC,耗时3-5秒,导致接口超时
// 排查工具:
// 1. jstat -gcutil 发现老年代在凌晨快速上升
// 2. GC日志分析发现并发失败(Concurrent Mode Failure)
// 3. jmap dump后发现大量缓存对象
// 问题原因:
// - 定时任务加载大量数据到缓存,老年代空间不足
// - CMS触发阈值过低(默认92%)
// 解决方案:
// -XX:+UseCMSInitiatingOccupancyOnly
// -XX:CMSInitiatingOccupancyFraction=70 // 提前触发并发收集
// -Xmx8g // 堆内存从4G扩大到8G
// 使用软引用缓存:new SoftReference<>(data)
// 优化效果:
// Full GC频率从每天1次降到每周1次,停顿时间从3秒降到1秒
}
/**
* 案例2:G1停顿时间超标调优
*/
public class G1PauseTimeTuning {
// 现象:G1目标200ms,但实际经常达到300-400ms
// 排查工具:
// 1. GC日志分析发现Humongous分配频繁
// 2. 查看G1Ergonomics日志发现Region选择过多
// 问题原因:
// - 大对象(>1MB)过多,每个占多个Region
// - 存活对象拷贝耗时超预期
// 解决方案:
// -XX:G1HeapRegionSize=32m // Region从默认2M调大到32M
// -XX:G1MixedGCCountTarget=16 // 增加Mixed GC次数,减少每次回收量
// -XX:G1ConfidencePercent=60 // 提高预测置信度
// 优化效果:
// 停顿时间稳定在180-220ms,达标率95%
}
/**
* 案例3:内存泄漏排查
*/
public class MemoryLeakCase {
// 现象:系统运行一周后变慢,最终OOM
// 排查过程:
// 1. jstat发现老年代持续增长,GC后不下降
// 2. jmap dump堆内存
// 3. MAT分析发现ThreadLocal对象占用80%内存
// 4. 查看GC Root链:Thread → ThreadLocalMap → Entry → value
// 问题原因:
// ThreadLocal使用后未remove,线程池中的线程长期存活,导致value无法回收
// 解决方案:
try {
threadLocal.set(data);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须remove
}
// 优化效果:
// 内存曲线平稳,再无OOM
}
/**
* 案例4:吞吐量优先调优
*/
public class ThroughputTuning {
// 场景:离线批处理系统,要求吞吐量最大化
// 目标:GC时间占比 < 1%
// 配置:
// -XX:+UseParallelGC
// -XX:ParallelGCThreads=8
// -Xmx16g -Xms16g
// -XX:NewRatio=2 // 新生代:老年代=1:2
// -XX:SurvivorRatio=8 // Eden:Survivor=8:1
// 优化效果:
// 吞吐量达到99.5%,GC时间仅占0.5%
}
/**
* 案例5:实时响应系统调优
*/
public class LowLatencyTuning {
// 场景:证券交易系统,要求低延迟
// 目标:STW < 100ms
// 配置:
// -XX:+UseG1GC
// -XX:MaxGCPauseMillis=100
// -XX:G1HeapRegionSize=16m
// -XX:ConcGCThreads=4 // 并发线程数
// -XX:+UnlockExperimentalVMOptions
// -XX:G1NewSizePercent=10 // 新生代初始比例
// 优化效果:
// 99%的GC停顿在80ms以内,满足业务要求
}
/**
* 调优效果量化对比
*/
public class TuningResult {
// 调优前:
// - Young GC: 50ms, 每10秒一次
// - Full GC: 3s, 每天5次
// - 吞吐量: 97%
// - TP99: 800ms
// 调优后:
// - Young GC: 30ms, 每20秒一次
// - Full GC: 1s, 每周1次
// - 吞吐量: 99.2%
// - TP99: 200ms
}
}
/**
* GC调优总结表
*
* 指标 理想值 监控工具 优化手段
* 吞吐量 > 99% jstat + GC日志 调整堆大小、回收器
* Young GC时间 < 100ms GC日志 优化新生代、TLAB
* Full GC时间 < 1s GC日志 减少对象晋升、整理碎片
* Young GC频率 10-30秒/次 jstat 调整Eden大小
* Full GC频率 几天1次 jstat 检查内存泄漏
* 老年代使用率 < 70% jmap + MAT 优化缓存、软引用
*
* 调优原则:
* 1. 先定位问题,再调整参数
* 2. 一次只改一个参数
* 3. 观察足够长时间
* 4. 监控指标验证效果
*/
// 面试金句
// "GC调优就像'系统体检',工具链就是'医疗设备':
// jstat是'心电图'(实时监控),GC日志是'病历本'(历史记录),
// jmap是'CT扫描'(堆dump),MAT是'病理分析'(内存诊断)。
// 关注指标就是'生命体征':吞吐量是'精力值',停顿时间是'心跳间隔',
// GC频率是'呼吸频率',内存变化是'体温曲线'。
// 在一次双11大促前,通过这套'体检流程'发现CMS并发失败风险,
// 提前调整触发阈值和堆大小,让系统'健康'度过高峰期。
// 调优的本质不是追求极致参数,而是让GC行为匹配业务场景。"
|