一、背景
一个部门其他同事的上线了很久的项目近期频繁的内存溢出——几乎每天内存溢出一次,而且频率越来越高。在添加了进程守护之后,虽然可以在内存溢出后自动重启,但并没有解决内存溢出的问题。不甘其扰之后,决定仔细排查导致内存溢出的根本原因。
二、排查过程
在将内存溢出的dump文件导出之后,通过Jprofiler进行分析,发现HashMap对象占用的内存很大,而且一直在增加。
就在代码里面查询创建全局HashMap对象的地方,发现有一个地方使用了ThreadLocal,代码如下:
private static final ThreadLocal<Map<Class<?>,Unmarshaller>> uMapLocal = new ThreadLocal<Map<Class<?>,Unmarshaller>>(){
@Override
protected Map<Class<?>, Unmarshaller> initialValue() {
return new HashMap<>();
}
};
这是一个微信回调时会使用的Map,往这个Map里面put数据的代码如下:
public static <T> T convertToObject(Class<T> clazz,Reader reader){
try {
Map<Class<?>, Unmarshaller> uMap = uMapLocal.get();
if(!uMapLocal.containsKey(clazz)){
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
uMapLocal.put(clazz, unmarshaller);
}
return (T) uMapLocal.get(clazz).unmarshal(reader);
} catch (JAXBException e) {
e.printStackTrace();
}
return null;
}
代码的本意是想避免对象的多次反序列化,想将已经反序列化过的对象放在一个全局的Map里面,下次如果这个Map中已经有了该对象就直接从Map里面获取,若没有则先将该对象反序列化之后置入这个Map中,再从该Map中获取。但他忽视了一点,这个HashMap是ThreadLocal的,微信在回调的时候每次都会新起一个线程,所以每次都会新创建一个HashMap对象。这就会导致内存泄漏。
三、修复
将原来创建全局HashMap的地方的ThreadLocal去掉即可:
private static final Map<Class<?>,Unmarshaller> uMapLocal = new HashMap<>();
四、TheadLocal的使用
ThreadLocal类型的变量从其命名上就可以知晓它是和线程有关的,每个线程各持有一份,并不是使用static修饰就变成了全局变量了。在使用的使用一定要注意clear。另外ThreadLocal类型的变量从一定意义上来说是可以用局部变量替换的,如果对ThreadLocal的原理不是很了解,最好不要使用,使用不当就可能导致内存泄漏问题。
五、排查过程中使用的工具和命令
上面将问题排查、定位的过程极大的简化了。下面说一下具体的排查、定位和解决的详细过程。
第一次该同事找我排查的时候,我将dump文件下载下来通过Jprofiler进行分析,显示是HashMap的内存占用很高,但HashMap对象并不是项目中定义的一个对象,并不好排查是哪个地方创建的HashMap。又再通过Jprofiler查看宕机时的线程的情况,定位到了出现问题的线程,然后查看代码,发现代码中有一个使用流的地方,但这个流在使用完之后没有关闭,就误以为是流未关闭导致的。在第一次修复时只是将这个流close掉了。
后来该同事跟我说没有解决问题,还是每天内存溢出。于是就安装了arthas,在生产环境的服务器上使用arthas工具的dashboard观察,发现每次minorgc之后老年代的内存都会增加1到2M的内存,我就意识到应该是有地方内存泄露了。
又查看当前堆内存中对象的创建情况:
jmap -histo:live 24353 >> /abc_class.log
结果和使用Jprofiler查看的一致,HashMap对象的数量惊人的庞大:
起初以为是项目中创建了一个全局的HashMap,然后不停的往该HashMap中put对象导致的。于是又从代码中找全局的HashMap,全部找出来之后没有发现存在一直向某个HashMap中put对象的问题。
再看上面的截图中,发现是HashMap对象自身的实例个数庞大,而不是一个HashMap的内存庞大,也就是说有一个地方在一直创建HashMap实例,但这个HashMap肯定不是简简单单的局部变量,因为局部变量在栈调用结束之后是可以被回收的。
最终终于找到了这个ThreadLocal的HashMap,解决了问题。