关于ThreadLocal内存泄漏的探讨

项目场景:

今天题主在查看关于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方法就会造成泄漏的原因。


  • 11
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值