项目场景:
今天题主在查看关于ThreadLocal的文章时,发现一个问题,就是网上主流的说法都是只要一个线程持有ThreadLocal对象时,在使用结束后不对ThreadLocal当中得数据进行remove就会造成内存泄漏。
但是题主今天在做实验时发现了这个其实时需要特定场景的,不是每个线程的ThreadLocal都会泄漏。而且题主在做实验时发现了另外一个问题,接下来题主就通过代码结合visualVM给大家演示演示一下多线程的ThreadLocal的内存占用和回收情况。
Note:题主使用的jdk版本是17,visualVM2.1.9
关于visualVM的安装下载安装
- visualVM是一个可视化的Jvm监控工具,对于低版本的jdk,是内置在jdk的文件夹当中的,目录在{jdk安装目录}\bin目录下。
- 对于高版本的jdk,已经移除了visualVM插件,需要大家在官网上下载:官网地址,安装完visualVM。我们需要配置visualVM监控哪个jdk,打开解压的visualVM安装包,进入{visualVM解压路径}/etc/visualvm.conf,将参数设置为visualvm_jdkhome=“your jdk path”。另外我们需要安装一个plugins,就是Visual Gc插件,这个插件可以很好的观测jvm堆内存数据的分布情况。对于如何Visual Gc插件的安装,打开visualVM—>打开Tools/plugins。因为题主的是高版本(题主2.1.9),可以直接在插件库当中选择,低版本的小伙伴需要在网上下载导入进来。
进入到plugins页面我么就可以看到所有的可用的plugins,大家按照自己需要的安装,我这边只安装Visual Gc。
- 安装完成后我们来编写我们的代码
public class HelloController {
//通过new Thread的形式去使用,threadLocal.set,每次设置100M的数据.
//每50ms创建一个新的线程
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
//主线程sleep 5秒钟,方便我们打开visualVM
mainThread.sleep(5000);
ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
while (true) {
//每隔十秒创建一个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Byte[] bytes = new Byte[1024 * 1024 * 100];
threadLocal.set(bytes);
// threadLocal.remove();
System.out.println("线程" + Thread.currentThread().getName() + "创建了一个100M大小的对象");
}
});
thread.start();
mainThread.sleep(50);
}
}
}
- 当我们启动代码后就可以在visualVM当中找到我们的这个线程了,双击这个线程。
- 我们可以看到,大对象直接分配到老年代了,没有在新生代当中停留,但是有个有趣的问题,老年代的对象一直在触发GC,说好的内存泄露了。而且我们的代码当中的线程已经创建了4千多个了,如果每个都是100M,早就内存泄漏了。
- 上述的情况是我们使用的new Thread去创建的线程,并且在线程当中去设置了ThreadLocal,并没有发生ThreadLocal内存泄漏。我们换个姿势去再试一遍,下面我使用线程池去创建使用。运行一下我们的代码,观察一下visualVm。
public class HelloController {
//通过Executors.newFixedThreadPool(100)创建100个线程。
//threadLocal.set,每次设置100M的数据,每50ms创建一个新的线程
public static void main(String[] args) throws InterruptedException {
Thread.sleep(20000);
ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
ExecutorService executorService = Executors.newFixedThreadPool(100);
while (true) {
executorService.submit(new Runnable() {
@Override
public void run() {
Byte[] bytes = new Byte[1024 * 1024 * 100];
threadLocal.set(bytes);
// threadLocal.remove();
System.out.println("线程" + Thread.currentThread().getName() + "创建了一个100M大小的对象");
}
});
Thread.sleep(50);
}
}
}
确实发现了内存泄漏,老年代的对象没有回收的迹象。题主在测试的过程钟还遇到了另外一个问题,题主的的代码没有报OOM。程序假死,没有新的线程去执行任务,在没有足够内存去分配时,不应该报错嘛,有点糊涂了。
主线程并未退出,但是线程池的任务并不会执行了,有知道的小伙伴可以评论区讨论一下,按照常理来说要么报错,要么OOM的。
原因分析:
根据上面的整个演示过程,我们不难发现,其实我们new Thread创建的线程不会造成ThreadLocal内存泄漏的,按照题主的理解,应该是因为这个线程已经结束了,jvm当中的线程对象被回收后,根据jvm的回收算法可达性性算法,线程引用的ThreadLocalMap就不可达了,就会进行回收。但是当我们使用了线程池时,线程池当中的线程是被线程池对象所持有的,线程池对象不回收。线程对象也就回收不了,就回导致ThreadLocal内存泄漏。一般我们在spring web项目当中使用的线程要么是tomcat服务器的线程,要么是集中管理的线程池对象,所以基本上不做ThreadLocal.remove()都会造成内存泄漏
。这个就是网上大家说的,使用了ThreadLocal不进行remove方法就会造成泄漏的原因。