260104-JVM面试题(内存泄露)

JVM 内存泄漏面试题完全指南

目录


一、问题背景

1.1 什么是内存泄漏

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

核心特征

  • ✅ 对象仍然被引用(GC Roots 可达)
  • ✅ 但业务上已不再需要这些对象
  • ✅ 随着时间推移,内存占用持续增长
  • ❌ GC 无法回收这些对象

1.2 内存泄漏 vs 内存溢出

对比项 内存泄漏 内存溢出
定义 对象未被及时释放 内存不足以满足分配需求
原因 代码逻辑缺陷 内存配置过小或负载过高
表现 内存缓慢增长 突然 OOM 异常
解决 修复代码逻辑 增加内存或优化算法
检测难度 ⚠️ 较难(需长期观察) ✅ 容易(直接报错)

关系:内存泄漏最终会导致内存溢出(OOM)

1.3 常见症状

🔍 生产环境表现

  1. Old Gen 使用率持续上升

    • 即使 Full GC 后也无法有效回收
    • 呈现阶梯式增长趋势
  2. Full GC 频率增加

    • 从每天几次增加到每小时几次
    • Full GC 耗时变长(秒级 → 分钟级)
  3. GC 回收率下降

    • Full GC 后老年代回收率 < 50%
    • 正常情况应 > 80%
  4. 应用响应变慢

    • RT(响应时间)逐渐增加
    • QPS(吞吐量)逐渐下降
  5. 最终 OOM

    1
    2
    3
    java.lang.OutOfMemoryError: Java heap space
    java.lang.OutOfMemoryError: Metaspace
    java.lang.OutOfMemoryError: Direct buffer memory

二、定位阶段

2.1 监控指标观察

首先通过监控体系(Prometheus + APM)观察 JVM 核心指标。

重点关注指标

1. Old Gen(老年代)使用率

1
2
# Prometheus 查询语句
jvm_memory_used_bytes{area="heap",id="Old Gen"} / jvm_memory_max_bytes{area="heap",id="Old Gen"} * 100

正常情况

  • Full GC 后降至 30%-50%
  • 曲线呈锯齿状(GC 后下降,使用后上升)

异常情况

  • Full GC 后仍维持在 70% 以上
  • 曲线持续上升,无明显下降

2. Full GC 频率与耗时

1
2
3
4
5
6
# 查看 GC 统计
jstat -gcutil <pid> 1000

# 输出示例
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 95.23 45.67 89.12 92.34 88.56 1234 12.345 56 45.678 58.023

关键字段

  • O(Old Gen):老年代使用率
  • FGC(Full GC 次数):短时间内快速增长
  • FGCT(Full GC 总耗时):单次耗时超过 1 秒需关注

3. GC 回收率

1
2
# 计算回收率
回收率 = (GC前内存 - GC后内存) / GC前内存 * 100%

正常值:> 80%
异常值:< 50%(说明大量对象无法回收)

2.2 初步判断

关联业务分析,排除正常高负载场景。

排查步骤

  1. 检查 QPS/RT 变化

    1
    2
    - QPS 没有明显突增 → 排除流量激增
    - RT 没有明显突增 → 排除慢查询/慢接口
  2. 检查业务操作

    1
    2
    3
    - 是否有大批量数据导入?
    - 是否有定时任务执行?
    - 是否有缓存预热操作?
  3. 综合判断

    1
    2
    如果 Old Gen 持续增长 + Full GC 频繁 + 回收率低 + 业务无异常
    → 初步判定为内存泄漏

2.3 GC 日志分析

同步查看 GC 日志,确认老年代对象异常滞留。

开启 GC 日志

1
2
3
4
5
# JDK 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log

# JDK 11+
-Xlog:gc*:file=/path/to/gc.log:time,uptime:filecount=5,filesize=10M

分析要点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 正常的 Full GC 日志
2024-01-04T10:00:00.000+0800: [Full GC (Ergonomics)
[PSYoungGen: 524288K->0K(1048576K)]
[ParOldGen: 3145728K->524288K(4194304K)] # 老年代从 3G 降到 512M
3670016K->524288K(5242880K),
[Metaspace: 102400K->102400K(1146880K)],
2.345 secs]

# 异常的 Full GC 日志(回收效果差)
2024-01-04T10:05:00.000+0800: [Full GC (Ergonomics)
[PSYoungGen: 524288K->0K(1048576K)]
[ParOldGen: 3670016K->3407872K(4194304K)] # 老年代从 3.5G 只降到 3.2G
4194304K->3407872K(5242880K),
[Metaspace: 102400K->102400K(1146880K)],
5.678 secs] # 耗时过长

异常特征

  • ❌ Full GC 后老年代回收很少(< 20%)
  • ❌ Full GC 耗时过长(> 3 秒)
  • ❌ Full GC 频率过高(几分钟一次)

三、分析阶段

3.1 生成 Heap Dump

摘除故障节点,确保不影响生产环境,然后生成 Heap Dump。

方法 1:jcmd 命令(推荐)

1
2
3
4
5
# 生成 Heap Dump
jcmd <pid> GC.heap_dump /path/to/heapdump.hprof

# 示例
jcmd 12345 GC.heap_dump /tmp/heapdump_20240104.hprof

方法 2:Arthas(在线诊断)

1
2
3
4
5
# 启动 Arthas
java -jar arthas-boot.jar

# 生成 Heap Dump(只包含存活对象,文件更小)
heapdump --live /tmp/heapdump_live.hprof

方法 3:JVM 参数自动 dump

1
2
# OOM 时自动生成 dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/

注意事项

  • ⚠️ Heap Dump 会暂停 JVM(Stop-The-World)
  • ⚠️ 文件大小约为堆内存的 1/3 ~ 1/2
  • ⚠️ 生产环境建议在低峰期操作
  • ✅ 使用 --live 参数只 dump 存活对象,减小文件大小

3.2 MAT 分析步骤

使用 Eclipse Memory Analyzer Tool(MAT)分析 Heap Dump。

步骤 1:打开 Heap Dump

1
File → Open Heap Dump → 选择 .hprof 文件

步骤 2:查看 Leak Suspects Report

MAT 会自动生成泄漏嫌疑报告,标注可能的泄漏点。

步骤 3:支配树(Dominator Tree)分析

1
Actions → Histogram → 按 Retained Heap 排序

关键概念

  • Shallow Heap:对象自身占用的内存
  • Retained Heap:对象及其引用的所有对象占用的总内存(更重要)

步骤 4:GC Roots 分析

→ Path to GC Roots → exclude weak/soft references```
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

找到是谁在引用这个对象,导致无法被 GC 回收。

#### 步骤 5:定位泄漏代码

通过调用栈信息,定位到具体的类和方法。

### 3.3 常见泄漏类型

#### 1. 堆内存泄漏(Heap Memory Leak)

最常见,对象在堆中未被及时释放。

**典型场景**:
- IO/DB/HTTP 资源未关闭
- ThreadLocal 存储对象未 remove
- 无界集合类且无失效策略
- 监听器/回调未注销

#### 2. Metaspace 泄漏

动态生成的类未被卸载。

**典型场景**:
- 频繁的反射操作
- 循环内创建 Proxy/CGLIB 代理
- Groovy/JavaScript 等脚本引擎重复编译
- 自定义 ClassLoader 未正确释放

**监控指标**:
```bash
jstat -gc <pid> 1000 | awk '{print $7}' # MC(Metaspace Capacity)
jstat -gc <pid> 1000 | awk '{print $8}' # MU(Metaspace Used)

3. DirectMemory 泄漏

堆外内存未被释放。

典型场景

  • Netty ByteBuf 未 release
  • NIO DirectByteBuffer 未清理
  • JNI 本地内存泄漏

监控指标

1
2
# 查看 DirectMemory 使用情况
jcmd <pid> VM.native_memory summary

四、修复方案

4.1 资源类泄漏修复

问题代码

1
2
3
4
5
6
7
// ❌ 错误:资源未关闭
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("test.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line = reader.readLine();
// 忘记关闭资源
}

修复方案

1
2
3
4
5
6
7
// ✅ 正确:使用 try-with-resources
public void readFile() throws IOException {
try (FileInputStream fis = new FileInputStream("test.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
String line = reader.readLine();
} // 自动关闭资源
}

连接池管理

1
2
3
4
5
6
7
8
9
10
// ✅ 使用 HikariCP 管理数据库连接
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(10);
config.setIdleTimeout(300000);
HikariDataSource dataSource = new HikariDataSource(config);

// ✅ 使用 OkHttp 连接池
OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();

4.2 ThreadLocal 泄漏修复

问题代码

1
2
3
4
5
6
7
8
// ❌ 错误:ThreadLocal 未清理
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<>();

public void handleRequest(User user) {
userContext.set(new UserContext(user));
// 业务逻辑...
// 忘记调用 remove()
}

修复方案 1:Filter/Interceptor 统一清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 正确:在 Filter 中统一管理
@Component
public class UserContextFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
User user = extractUser(request);
UserContext context = new UserContext(user);
UserContextHolder.set(context);

chain.doFilter(request, response);
} finally {
// 确保清理
UserContextHolder.remove();
}
}
}

修复方案 2:AOP 切面统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 使用 AOP 自动清理
@Aspect
@Component
public class ThreadLocalCleanAspect {

@Around("@annotation(CleanThreadLocal)")
public Object cleanThreadLocal(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} finally {
// 清理所有 ThreadLocal
UserContextHolder.remove();
RequestContextHolder.remove();
}
}
}

最佳实践

  • ✅ 使用 try-finally 确保 remove() 被调用
  • ✅ 在 Filter/Interceptor/AOP 中统一管理
  • ✅ 线程池场景下必须清理(线程复用会导致泄漏)

4.3 缓存/集合泄漏修复

问题代码

1
2
3
4
5
6
// ❌ 错误:无界集合
private static final Map<String, Object> cache = new HashMap<>();

public void addToCache(String key, Object value) {
cache.put(key, value); // 无限增长
}

修复方案 1:使用 Caffeine 缓存

1
2
3
4
5
6
7
8
9
10
11
// ✅ 正确:带淘汰策略的缓存
private static final Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 最大容量
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后 5 分钟过期
.recordStats() // 记录统计信息
.build();

public void addToCache(String key, Object value) {
cache.put(key, value);
}

修复方案 2:使用 Guava Cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ Guava Cache
private static final LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(notification -> {
log.info("Key {} removed, reason: {}",
notification.getKey(), notification.getCause());
})
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) throws Exception {
return loadDataFromDB(key);
}
});

修复方案 3:限制静态集合大小

1
2
3
4
5
6
7
// ✅ 使用 LinkedHashMap 实现 LRU
private static final Map<String, Object> lruCache = new LinkedHashMap<String, Object>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 1000; // 超过 1000 个元素时移除最老的
}
};

4.4 Metaspace 泄漏修复

问题代码

1
2
3
4
5
6
7
8
9
// ❌ 错误:循环内创建动态代理
for (int i = 0; i < 10000; i++) {
// 每次循环都创建新的代理类
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[]{MyInterface.class},
new MyInvocationHandler()
);
}

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 正确:复用代理类
private static final Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>();

public <T> T getProxy(Class<T> interfaceClass) {
return (T) proxyCache.computeIfAbsent(interfaceClass, clazz ->
Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[]{clazz},
new MyInvocationHandler()
)
);
}

JVM 参数配置

1
2
3
4
5
6
7
# 限制 Metaspace 大小
-XX:MaxMetaspaceSize=256m

# Metaspace 达到阈值时触发 GC
-XX:MetaspaceSize=128m
-XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio=70

4.5 DirectMemory 泄漏修复

问题代码

1
2
3
4
5
// ❌ 错误:Netty ByteBuf 未释放
ByteBuf buf = Unpooled.buffer(1024);
buf.writeBytes(data);
channel.writeAndFlush(buf);
// 忘记 release

修复方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 正确:手动释放
ByteBuf buf = Unpooled.buffer(1024);
try {
buf.writeBytes(data);
channel.writeAndFlush(buf);
} finally {
// 如果 writeAndFlush 失败,需要手动释放
if (buf.refCnt() > 0) {
buf.release();
}
}

// ✅ 更好的方式:使用 ReferenceCountUtil
ByteBuf buf = Unpooled.buffer(1024);
buf.writeBytes(data);
ReferenceCountUtil.releaseLater(channel.writeAndFlush(buf));

JVM 参数配置

1
2
# 限制 DirectMemory 大小
-XX:MaxDirectMemorySize=512m

监控 DirectMemory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 查看 Native Memory Tracking
jcmd <pid> VM.native_memory summary

# 输出示例
Native Memory Tracking:
Total: reserved=2GB, committed=1.5GB
Java Heap: reserved=1GB, committed=800MB
Class: reserved=256MB, committed=128MB
Thread: reserved=128MB, committed=64MB
Code: reserved=64MB, committed=32MB
GC: reserved=128MB, committed=64MB
Compiler: reserved=32MB, committed=16MB
Internal: reserved=64MB, committed=32MB
Symbol: reserved=32MB, committed=16MB
Native Memory Tracking: reserved=16MB, committed=8MB
Arena Chunk: reserved=8MB, committed=4MB
Unknown: reserved=64MB, committed=32MB

五、验证阶段

5.1 灰度发布策略

采用渐进式发布,降低风险。

发布流程

1
2
3
4
5
第 1 天:10% 节点 → 观察核心指标
↓ 无异常
第 2-3 天:30% 节点 → 继续观察
↓ 无异常
第 4-7 天:100% 节点 → 全量发布

回滚预案

  • 如果 Old Gen 再次快速增长 → 立即回滚
  • 如果 Full GC 频率异常 → 立即回滚
  • 保留旧版本镜像至少 7 天

5.2 核心指标监控

持续观察 3~7 天,重点关注以下指标。

监控看板

1. Old Gen 曲线

正常情况

  • ✅ Full GC 后降至 30%-50%
  • ✅ 曲线平稳,无持续增长趋势
  • ✅ 波动范围在合理区间(±10%)

异常情况

  • ❌ Full GC 后仍高于 70%
  • ❌ 曲线持续上升(斜率 > 0)
  • ❌ 波动幅度越来越大

2. Full GC 频率

正常情况

  • ✅ 每天 0-2 次(低负载系统)
  • ✅ 每小时 0-1 次(高负载系统)
  • ✅ 单次耗时 < 1 秒

异常情况

  • ❌ 每分钟多次 Full GC
  • ❌ 单次耗时 > 3 秒
  • ❌ Full GC 间隔越来越短

3. GC 回收率

正常情况

  • ✅ Young GC 回收率 > 90%
  • ✅ Full GC 回收率 > 80%

异常情况

  • ❌ Full GC 回收率 < 50%
  • ❌ 回收率持续下降

监控告警配置

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
# Prometheus AlertManager 配置
groups:
- name: jvm-memory-alerts
rules:
# Old Gen 使用率告警
- alert: OldGenHighUsage
expr: jvm_memory_used_bytes{area="heap",id="Old Gen"} / jvm_memory_max_bytes{area="heap",id="Old Gen"} * 100 > 80
for: 10m
labels:
severity: warning
annotations:
summary: "Old Gen 使用率超过 80%"
description: "{{ $labels.instance }} Old Gen 使用率: {{ $value }}%"

# Full GC 频率告警
- alert: FrequentFullGC
expr: rate(jvm_gc_collection_seconds_count{gc="G1 Old Generation"}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Full GC 过于频繁"
description: "{{ $labels.instance }} 5 分钟内 Full GC 次数: {{ $value }}"

# GC 回收率告警
- alert: LowGCReclaimRate
expr: (jvm_memory_used_bytes{area="heap",id="Old Gen"} - jvm_memory_committed_bytes{area="heap",id="Old Gen"}) / jvm_memory_used_bytes{area="heap",id="Old Gen"} * 100 < 50
for: 15m
labels:
severity: warning
annotations:
summary: "GC 回收率过低"
description: "{{ $labels.instance }} GC 回收率: {{ $value }}%"

六、复盘与预防

6.1 代码规范

制定并强制执行代码规范,从源头避免内存泄漏。

规范清单

1. 资源关闭规范

1
2
3
4
5
6
// ✅ 强制使用 try-with-resources
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 业务逻辑
}

Code Review 检查点

  • 所有 IO 资源是否使用 try-with-resources?
  • 数据库连接是否正确关闭?
  • HTTP 客户端是否使用连接池?

2. ThreadLocal 清理规范

1
2
3
4
5
6
7
// ✅ 强制在 finally 块中清理
try {
UserContextHolder.set(userContext);
// 业务逻辑
} finally {
UserContextHolder.remove();
}

Code Review 检查点

  • 所有 ThreadLocal 是否有对应的 remove()?
  • 是否在 finally 块中清理?
  • 线程池场景下是否正确清理?

3. 缓存容量限制规范

1
2
3
4
5
// ✅ 强制设置最大容量和过期时间
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 必须设置
.expireAfterWrite(10, TimeUnit.MINUTES) // 必须设置
.build();

Code Review 检查点

  • 所有缓存是否设置了 maximumSize?
  • 是否设置了过期策略?
  • 是否避免了全局 static 集合?

6.2 静态代码扫描

CI 集成静态代码分析工具,自动拦截可疑模式。

工具选型

工具 优势 适用场景
SonarQube 功能全面,支持多语言 企业级项目
SpotBugs 专注于 Java Bug 检测 Java 项目
PMD 规则丰富,可定制 代码规范检查
Checkstyle 代码风格检查 编码规范

集成方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# GitLab CI 配置
stages:
- test
- scan

sonarqube-check:
stage: scan
image: sonarsource/sonar-scanner-cli:latest
script:
- sonar-scanner
-Dsonar.projectKey=my-project
-Dsonar.sources=src/main/java
-Dsonar.host.url=http://sonarqube.example.com
-Dsonar.login=${SONAR_TOKEN}
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

spotbugs-check:
stage: scan
script:
- mvn spotbugs:check
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

自定义规则示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- SpotBugs 自定义规则 -->
<FindBugsFilter>
<!-- 检测 ThreadLocal 未清理 -->
<Match>
<Bug pattern="TLW_TWO_LOCK_WAIT_NOTIFY" />
</Match>

<!-- 检测资源未关闭 -->
<Match>
<Bug pattern="OS_OPEN_STREAM" />
</Match>

<!-- 检测无界集合 -->
<Match>
<Bug pattern="DM_NUMBER_CTOR" />
</Match>
</FindBugsFilter>

6.3 监控告警

建立完善的监控告警体系,提前发现潜在问题。

告警策略

1. Old Gen 增长率告警

1
2
3
4
5
6
7
8
9
10
11
# 检测 Old Gen 增长速度
- alert: OldGenGrowthRate
expr: |
derivative(jvm_memory_used_bytes{area="heap",id="Old Gen"}[1h])
/ jvm_memory_max_bytes{area="heap",id="Old Gen"} * 100 > 5
for: 2h
labels:
severity: warning
annotations:
summary: "Old Gen 增长率过快"
description: "{{ $labels.instance }} 过去 2 小时 Old Gen 增长率: {{ $value }}%/h"

2. Full GC 频次告警

1
2
3
4
5
6
7
8
9
10
# 检测 Full GC 频率
- alert: HighFullGCFrequency
expr: |
increase(jvm_gc_collection_seconds_count{gc="G1 Old Generation"}[1h]) > 10
for: 30m
labels:
severity: critical
annotations:
summary: "Full GC 频率过高"
description: "{{ $labels.instance }} 过去 1 小时 Full GC 次数: {{ $value }}"

3. Metaspace 使用率告警

1
2
3
4
5
6
7
8
9
10
11
# 检测 Metaspace 使用率
- alert: MetaspaceHighUsage
expr: |
jvm_memory_used_bytes{area="nonheap",id="Metaspace"}
/ jvm_memory_max_bytes{area="nonheap",id="Metaspace"} * 100 > 85
for: 15m
labels:
severity: warning
annotations:
summary: "Metaspace 使用率过高"
description: "{{ $labels.instance }} Metaspace 使用率: {{ $value }}%"

On-Call 巡检清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
## 每日巡检清单

### JVM 内存检查
- [ ] Old Gen 使用率 < 70%
- [ ] Metaspace 使用率 < 80%
- [ ] DirectMemory 使用率 < 80%

### GC 检查
- [ ] Full GC 次数 < 10 次/天
- [ ] Full GC 平均耗时 < 1
- [ ] GC 回收率 > 80%

### 应用健康检查
- [ ] QPS 波动 < 20%
- [ ] RT P99 < 500ms
- [ ] 错误率 < 0.1%

### 日志检查
- [ ] 无 OOM 异常
- [ ] 无 Full GC 警告
- [ ] 无资源泄漏警告

七、面试回答模板

7.1 标准回答框架

面试官问:“如何定位和解决内存泄漏问题?”

回答框架

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
我会按照「定位 → 分析 → 修复 → 验证 → 复盘」五步法来处理:

第一步【定位】:
通过监控体系观察 JVM 核心指标,重点关注 Old Gen 使用率趋势、
Full GC 频率与耗时、GC 回收率。同时关联业务分析,如果 QPS/RT
没有明显突增,初步判定为内存泄漏而非正常高负载。此时同步查看
GC 日志与 jstat -gcutil,确认老年代对象异常滞留。

第二步【分析】:
摘除故障节点,使用 jcmd 或 Arthas 生成 Heap Dump,然后用 MAT
通过支配树和 GC Roots 分析出泄漏对象。常见的泄漏类型包括:
资源未关闭、ThreadLocal 未清理、无界集合、Metaspace 泄漏、
DirectMemory 泄漏等。

第三步【修复】:
针对不同泄漏类型采取对应措施:
- 资源类:使用 try-with-resources 或连接池
- ThreadLocal:在 Filter/Interceptor 的 finally 块强制 remove
- 缓存/集合:使用 Caffeine/Guava Cache,设置容量和过期策略
- Metaspace:控制动态类生成,配置 MaxMetaspaceSize
- DirectMemory:检查 Netty ByteBuf release,配置 MaxDirectMemorySize

第四步【验证】:
采用灰度发布策略(10% → 30% → 100%),持续观察 3-7 天。
重点关注 Old Gen 曲线是否平稳、Full GC 频率是否正常、
回收率是否恢复到 80% 以上。

第五步【复盘】:
总结泄漏原因,完善代码规范(资源关闭、ThreadLocal 清理、
缓存容量限制),CI 集成 SonarQube/SpotBugs 静态扫描,
配置监控告警(Old Gen 增长率、Full GC 频次),纳入 On-Call
巡检清单,形成闭环。

7.2 加分项

展示深度思考

  1. 提到具体工具链

    1
    2
    "我们团队使用 Prometheus + Grafana 监控 JVM 指标,
    配合 Arthas 在线诊断,MAT 离线分析,形成完整的诊断链路。"
  2. 提到实际案例

    1
    2
    3
    "我之前遇到过 ThreadLocal 在线程池场景下的泄漏问题,
    原因是线程复用导致 ThreadLocal 值累积。通过在
    ThreadPoolExecutor 的 afterExecute 钩子中统一清理解决。"
  3. 提到预防措施

    1
    2
    3
    4
    5
    "除了事后处理,我们更注重事前预防:
    - Code Review 强制检查资源关闭和 ThreadLocal 清理
    - CI 集成 SpotBugs 自动检测可疑模式
    - 压测时专门监控内存增长趋势
    - 定期(每季度)进行内存泄漏专项排查"
  4. 提到性能权衡

    1
    2
    3
    4
    "在修复时要注意性能影响,比如:
    - try-with-resources 比手动 close 更安全,但略有性能开销
    - Caffeine 比 HashMap 多了淘汰策略,但内存占用更可控
    - 需要在安全性和性能之间找到平衡点"

参考资料

#
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×