记一次TheadLocal使用方式不正确导致内存泄漏问题的排查和修复过程

编程入门 行业动态 更新时间:2024-10-26 14:31:19

记一次TheadLocal使用方式<a href=https://www.elefans.com/category/jswz/34/1771406.html style=不正确导致内存泄漏问题的排查和修复过程"/>

记一次TheadLocal使用方式不正确导致内存泄漏问题的排查和修复过程

一、背景

  一个同事上线了很久的项目近期频繁的内存溢出——几乎每天内存溢出一次,而且频率越来越高。在添加了进程守护之后,虽然可以在内存溢出后自动重启,但并没有解决内存溢出的问题。不甘其扰之后,决定仔细排查导致内存溢出的根本原因。

二、排查过程

  在将内存溢出的dump文件导出之后,通过Jprofiler进行分析,发现HashMap对象占用的内存很大,而且一直在增加。
  就在代码里面搜索创建全局HashMap对象的地方,发现有一个地方使用了ThreadLocal,代码如下:

private static final ThreadLocal<Map<Class<?>,Unmarshaller>> uMapLocal = new ThreadLocal<Map<Class<?>,Unmarshaller>>(){@Overrideprotected 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(!uMap.containsKey(clazz)){JAXBContext jaxbContext = JAXBContext.newInstance(clazz);Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();uMap.put(clazz, unmarshaller);}return (T) uMap.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修饰就变成了全局变量了。另外ThreadLocal类型的变量从一定意义上来说是可以用局部变量替换的,如果对ThreadLocal的原理不是很了解,最好不要使用,使用不当就可能导致内存泄漏问题。

五、排查过程中使用的工具和命令

  上面将问题排查、定位的过程极大的简化了。下面说一下具体的排查、定位和解决的详细过程。
  第一次该同事找我排查的时候,我将dump文件下载下来通过Jprofiler进行分析,显示是HashMap的内存占用很高,但HashMap并不是项目中定义的一个类,否则可以通过包名或类名进行筛查,这是一个使用频率很高的通用类型,并不好排查是在哪个地方创建的。又再通过Jprofiler查看宕机时的线程的情况,定位到了出现问题的线程,也定位到了出现问题的代码块。然后查看代码,发现代码中有一个使用流的地方,但这个流在使用完之后没有关闭,就误以为是流未关闭导致的。在第一次修复时只是将这个流close掉了。


  Tips:如果是一个经验老道的人看上面两个图应该就可以定位到问题了,起码不会瞎猜是流未关闭导致的。
  后来该同事跟我说没有解决问题,还是每天内存溢出。于是就安装了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类型的变量。
  最终终于找到了这个ThreadLocal的HashMap,解决了问题。修复之后,再通过arthas的dashboard观察,发现老年代的内存不再随着minorgc增大了。

更多推荐

记一次TheadLocal使用方式不正确导致内存泄漏问题的排查和修复过程

本文发布于:2023-12-06 16:53:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1668214.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:不正确   内存   过程   方式   TheadLocal

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!