Skip to content

Commit ed517f2

Browse files
author
chaodongyue
committed
add SSLSocketImpl-memory-leak
1 parent 7a946e9 commit ed517f2

File tree

9 files changed

+108
-0
lines changed

9 files changed

+108
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
title: SSLSocketImpl 内存泄漏
3+
date: 2025-12-15 9:00:00 +0800
4+
categories: [Blogging, Java]
5+
tags: [java]
6+
---
7+
8+
# 发现问题
9+
线上有个系统最近频繁的爆出FullGC
10+
11+
# 定位问题
12+
通过监控平台可以查看到JVM内存使用信息
13+
14+
![alt text](../assets/img/post/2025-12-15/1.png){: .normal}
15+
![alt text](../assets/img/post/2025-12-15/2.png){: .normal}
16+
17+
该JVM堆分配了4G的内存, 年老代已经占用了2G. 并且在FullGC后年老代内存大量降低, 可以推断出, 大量的对象来不及在年轻代里面回收从而进入了年老代.
18+
19+
登上服务器执行下面的命令把当前堆的所有对象信息dump下来
20+
```
21+
#如果不加 -all 的话会先执行一次FullGC
22+
jcmd ${pid} GC.heap_dump -all /tmp/heap.bin
23+
```
24+
25+
然后用Eclipse Memory Analyzer (MAT)打开, 通过dominator tree可以看到, 75%的内存对象都是被 java.lang.ref.Finalizer持有了.
26+
27+
回顾一下 java.lang.ref.Finalizer 的知识可以知道, 当一个对象被回收时, 如果重写了java.lang.Object#finalize(), 改对象被垃圾回收器回收时会被放进java.lang.ref.Finalizer 的 queue 里面作最后的释放对象处理.
28+
29+
java.lang.ref.Finalizer 的 queue 是由一个偏高优先级守护线程来消费,
30+
从而知道, 有大量的对象需要执行 finalize() 方法堆积在一起来不及消费, 从而长时间地存活在系统里面, 导致被转移到年老代.
31+
```java
32+
//创建线程
33+
ThreadGroup tg = Thread.currentThread().getThreadGroup();
34+
for (ThreadGroup tgn = tg;
35+
tgn != null;
36+
tg = tgn, tgn = tg.getParent());
37+
Thread finalizer = new FinalizerThread(tg);
38+
finalizer.setPriority(Thread.MAX_PRIORITY - 2);//偏高优先级, 默认是5
39+
finalizer.setDaemon(true);//守护线程
40+
finalizer.start();
41+
42+
43+
private static class FinalizerThread extends Thread {
44+
private volatile boolean running;
45+
FinalizerThread(ThreadGroup g) {
46+
super(g, "Finalizer");
47+
}
48+
public void run() {
49+
// in case of recursive call to run()
50+
if (running)
51+
return;
52+
53+
// Finalizer thread starts before System.initializeSystemClass
54+
// is called. Wait until JavaLangAccess is available
55+
while (!VM.isBooted()) {
56+
// delay until VM completes initialization
57+
try {
58+
VM.awaitBooted();
59+
} catch (InterruptedException x) {
60+
// ignore and continue
61+
}
62+
}
63+
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
64+
running = true;
65+
//循环消费队列数据
66+
for (;;) {
67+
try {
68+
Finalizer f = (Finalizer)queue.remove();
69+
f.runFinalizer(jla);
70+
} catch (InterruptedException x) {
71+
// ignore and continue
72+
}
73+
}
74+
}
75+
}
76+
```
77+
# 是什么对象被长期持有?
78+
究竟是什么东西被大量创建然后又被大量地等待执行 finalize() ?
79+
查看 Finalizer 类可以知道字段 referent 就是需要执行finalize()方法的对象.
80+
81+
通过MAT 的OQL查询出 referent 有哪些对象
82+
```
83+
SELECT referent FROM java.lang.ref.Finalizer
84+
```
85+
![alt text](../assets/img/post/2025-12-15/4.png){: .normal}
86+
87+
然后把结果导出再统计一下类, 发现有一半的对象都是被SSLSocketImpl持有, 而SSLSocketImpl又持有了SSLSessionImpl
88+
89+
sun.security.ssl.SSLSocketImpl 总数: 4833
90+
91+
sun.security.ssl.SSLSessionImpl 总数: 5064
92+
93+
再回到MAT, 通过outgoing查看这些对象持有了什么对象, 可以看到, 大部分host都是"oapi.dingtalk.com"
94+
![alt text](../assets/img/post/2025-12-15/5.png){: .normal}
95+
![alt text](../assets/img/post/2025-12-15/6.png){: .normal}
96+
97+
# 根本原因
98+
通过翻查我们自身业务代码, 有一个业务场景需要大量调用钉钉SDK 二方包接口发送钉钉通知. 这是一个HTTPS的接口, 每调用一次会new URL() 去请求. 导致大量通知时会有大量的连接创建, 大量的连接等待释放.
99+
100+
![alt text](../assets/img/post/2025-12-15/7.png){: .normal}
101+
102+
URL -> SSLSocketImpl 调用链
103+
![alt text](../assets/img/post/2025-12-15/8.png){: .normal}
104+
105+
# 解决方案
106+
1. 垃圾回收切换到G1, G1会有一个Mix GC阶段, 会把一部分年老代的对象回收. 这是最快的解决方案, 但也只是缓解
107+
2. 根本解决方法1, 联系钉钉团队提供, 这方法依赖其他团队进度, 对我们自身系统影响较大
108+
3. 根本解决方法2, 采用连接池方式请求, 自己写Http请求

assets/img/post/2025-12-15/1.png

22.9 KB
Loading

assets/img/post/2025-12-15/2.png

20.7 KB
Loading

assets/img/post/2025-12-15/3.png

18.9 KB
Loading

assets/img/post/2025-12-15/4.png

68.1 KB
Loading

assets/img/post/2025-12-15/5.png

74.5 KB
Loading

assets/img/post/2025-12-15/6.png

46.7 KB
Loading

assets/img/post/2025-12-15/7.png

77.4 KB
Loading

assets/img/post/2025-12-15/8.png

168 KB
Loading

0 commit comments

Comments
 (0)