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

15 篇文章 0 订阅
5 篇文章 0 订阅

一、背景

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

二、排查过程

  在将内存溢出的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(!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实例不会被GC,也就是说这个HashMap肯定不是一个简简单单的局部变量,因为局部变量在栈调用结束之后是可以被回收的;再仔细想一想,这个变量也不可能是一个简简单单的类变量,因为类变量只会随着类的加载初始化一次;也不可能是一个实例变量,因为实例变量的创建需要和实例本身一起创建,也就意味着应该同时有一个数量庞大的另一个实例,但现状并非如此。所以只能是一个和线程有关的ThreadLocal类型的变量。
  最终终于找到了这个ThreadLocal的HashMap,解决了问题。修复之后,再通过arthas的dashboard观察,发现老年代的内存不再随着minorgc增大了。
请添加图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava中的一个类,用于实现线程局部变量。它可以在每个线程中创建一个变量的副本,每个线程对该副本进行操作,互不影响。 当线程第一次调用ThreadLocal的set或者get方法时,会创建一个threadLocals变量,用于存储该线程的本地变量。具体而言,ThreadLocal实例本身相当于一个装载本地变量的工具壳,通过set方法将值添加到调用线程的threadLocals中,当调用线程调用get方法时,能够从自己的threadLocals中取出该变量。 在get方法的实现中,首先获取当前调用线程,如果当前线程的threadLocals不为null,就直接返回当前线程的threadLocals变量中的本地变量值,否则执行setInitialValue方法来初始化threadLocals变量。 需要注意的是,如果调用线程一直不终止,那么该本地变量将一直存放在threadLocals中,可能会导致内存溢出。因此,在使用ThreadLocal后,需要调用remove方法将该线程的threadLocals中的本地变量删除。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [ThreadLocal详解](https://blog.csdn.net/m0_49508485/article/details/123234587)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值