前言
很早之前虽然看过 ThreadLocal 的源码,但是对于真实业务场景下可能存在的问题没有做过总结,刚好前几天在分析 Mybatis 内存泄漏的问题,想着 ThreadLocal 不是也可能会发生内存泄漏吗?于是乎本文出现了。
本文相关博客
1 : ThreadLocal还存在内存泄漏?源码级别解读
2 : 高质量实现单文件导入、导出功能(使用EasyExcel )
大白话介绍:什么是内存泄漏?
内存泄漏:我个人理解就是由于程序设计失误,用完的内存空间没有得到应得的释放,这就是内存泄漏。
1 . 而我们要知道程序的运行需要被分配内存,简单来说我们每创建一个 List 、Map …并往里面塞数据的时候,都需要开辟内存空间来存储,当你只顾着塞数据,而不清空数据的时候,内存只会只增不减,当达到了物理机最大内存时就会发生 OOM。所以我们在编写代码的时候需要注意是否会出现内存泄漏。
2 . 从 JVM 角度 OOM 的出现是因为,Eden 区发生 Young Gc的时候,会进行回收 Eden 、 Surviver0 、 Surviver1 区里面没有被使用的对象的堆内存,而 Eden 区那些正在使用的对象会从 Eden 区,放到空的 Surviver 的 To 区里面,此时 Surviver 区存在俩种情况,一种是那些正在使用的对象年龄计数器达到了 15 会从 Surviver 区转到 老年代,还有一种情况就是 Surviver 区满了,放不下 Eden 区过来的对象了,这个对象直接放到老年代。当老年代满了的时候触发 Old Gc ,GC 完了之后发现老年代仍然放不下新生代过来的数据,此时就会爆堆空间满(OOM)的错误
线程池复用线程 bug 举例一
理论上:有源源不断的不同的线程一直往 ThreadLocal 中 set 数据,而且你还没有进行 Remove 方法的调用是会造成 OOM,但是现在主流的 Spring Boot 开发内置了一个 Tomcat 里面的线程是复用的,也就是说 10 个请求里面就有可能 2 个请求是同个线程发起的。我觉得即使是有可能造成内存泄漏 OOM,这个问题暴露的几率也很低,因为同线程多次 Set 值会被覆盖,但是这样会引申出另外一个问题,就是由于内置 TomCat 线程的复用,让原本线程隔离的 ThreadLocal 变得不线程隔离了。造成一些脏数据泄漏。只要是涉及到线程池线程复用的地方都会存在这个问题。那怎么去解决呢?答:当前线程代码逻辑执行完调用 Remove 方法就行。可以看下图,执行了 4 个异步任务,异步任务 2 居然也拿到了数据。原因是 CompletableFuture 内部也是维护了一个线程池,任务 1 和任务 2 都是复用的一个线程,所以都打印了 zzh1。
Spring Boot 内置 tomcat 线程复用 bug 举例二
编写如下俩个接口,按道理来说只有 getInfo 才会返回 test,getInfo2 返回 null 才对。
测试结果如下:俩个接口都返回了 test 值诶,原因就是内置 TomCat 复用了线程。
附页彩蛋
可能大家在本地测试:Spring Boot 内置 tomcat 线程复用 bug 举例二 中的现象的时候,getInfo 2有的时候是返回 null,而有的时候返回的却是 test 这个诡异现象,那是因为我忘记告诉大家要设置一个 Tomcat 的一个参数,最大线程数设置成 1 这样测试效果就明显了。
server:
port: 1133
tomcat:
threads:
max: 2
总结
避免内存泄漏的最好解决办法就是使用完 ThreadLoacl 中的数据的时候,同时调用一下 Remove 方法。其实之前我在写多线程导入 Excel 的源码里面也使用到了 ThreadLocal 存放每个线程解析到的 Excel 数据,如果那里不进行 Remove 方法的调用,一次几十万的数据解析出来内存一直占着而不进行释放,多个几次导入就很容易造成 OOM 了。大家注意一下就好了。
小咸鱼的技术窝
关注不迷路,日后分享更多技术干货,B站、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页