|
| 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 | +{: .normal} |
| 15 | +{: .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 | +{: .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 | +{: .normal} |
| 95 | +{: .normal} |
| 96 | + |
| 97 | +# 根本原因 |
| 98 | +通过翻查我们自身业务代码, 有一个业务场景需要大量调用钉钉SDK 二方包接口发送钉钉通知. 这是一个HTTPS的接口, 每调用一次会new URL() 去请求. 导致大量通知时会有大量的连接创建, 大量的连接等待释放. |
| 99 | + |
| 100 | +{: .normal} |
| 101 | + |
| 102 | +URL -> SSLSocketImpl 调用链 |
| 103 | +{: .normal} |
| 104 | + |
| 105 | +# 解决方案 |
| 106 | +1. 垃圾回收切换到G1, G1会有一个Mix GC阶段, 会把一部分年老代的对象回收. 这是最快的解决方案, 但也只是缓解 |
| 107 | +2. 根本解决方法1, 联系钉钉团队提供, 这方法依赖其他团队进度, 对我们自身系统影响较大 |
| 108 | +3. 根本解决方法2, 采用连接池方式请求, 自己写Http请求 |
0 commit comments