问题现象:
1 线程池中的线程,内存占用持续增长,通过 Jvisualvm 观察进程运行状态,发现 JVM 老年代内存最终会被耗尽,进而导致进程频繁 Full GC,CPU 资源几乎全部用于垃圾回收。
2 通过MAT(Memory Analyzer Tool)分析heapdump文件,发现线程对象的保留集(retained set)多大。线程池中有1500个线程,线程对象的堆内存几乎全部被 threadlocals 对象占用。
问题分析
1 threadlocals 在什么情况下会造成内存泄露?
当 ThreadLocals 对象通过 set 方法复制后,没有通过remove方法销毁,且线程由线程池管理,生命周期比较长时,将必然发生内存泄露。
2 通过查看 VMware vSphere 6.0 官方文档,编写简单的Demo 实例,连接到网络中的虚拟环境,并获取信息。
调试发现,当 new VimService() 创建服务时,会创建 XMLStreamReaderImpl对象。
由图可见,在创建VimService对象时,创建了XMLInputFactoryImpl实例(ThreadLocal对象),但是服务创建后,该ThreadLocal对象并没有通过remove函数销毁,导致无法通过垃圾回收机制回收线程中的内存资源。
通过函数栈可发现,内存泄露并不是VimService直接导致的,而是 com.sun.xml.internal.ws.wsdl.parser.RuntimeWSDLParser 类额 parse() 函数在进行 xml 解析时,没有合理释放 ThreadLocal 对象资源。由于很难查找到相应版本的源码文件且时间有限,没有对源文件进行深入分析,也因此没有定位发生 ThreadLocal 相关内存泄露的根本原因。
3 虚拟化 2.0 VS 虚拟化 3.0
Vim2.5 使用了 new VimService() 来创建服务,而老版本通过 new VimServiceLocator() 创建服务。
这也是为什么老版本能正常运行,而新版本发生内存泄露。
解决方案:
1 通过 Vim2.5 api 释放 ThreadLocal 资源:
经过多次尝试,并没有找到合适的接口!
由于时间关系,没有深入阅读Vim2.5和WSServiceDelegate的源码,或许有更正规的方式释放资源,请网友们继续探索。
2 通过反射机制,在线程层面主动释放 ThreadLocal 对象:
线程池中的线程会反复调用实现Runnable的类的run()方法,来执行任务。ThreadLocal对象的生命周期本来就应该在 run() 方法执行完成后结束。现在通过反射来手动清楚 ThreadLocal 对象。
源码如下:
private void cleanThreadLocals() {
try {
// Get a reference to the thread locals table of the current thread
Thread thread = Thread.currentThread();
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocalTable = threadLocalsField.get(thread);
// Get a reference to the array holding the thread local variables inside the
// ThreadLocalMap of the current thread
Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Field tableField = threadLocalMapClass.getDeclaredField("table");
tableField.setAccessible(true);
Object table = tableField.get(threadLocalTable);
// The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
// is a reference to the actual ThreadLocal variable
Field referentField = Reference.class.getDeclaredField("referent");
referentField.setAccessible(true);
for (int i=0; i < Array.getLength(table); i++) {
// Each entry in the table array of ThreadLocalMap is an Entry object
// representing the thread local reference and its value
Object entry = Array.get(table, i);
if (entry != null) {
// Get a reference to the thread local object and remove it from the table
ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
threadLocal.remove();
}
}
} catch(Exception e) {
// We will tolerate an exception here and just log it
throw new IllegalStateException(e);
}
}