• 一文搞懂ThreadLocal内存泄漏问题


    1.什么是内存泄漏问题?

    内存泄漏 表示就是我们申请了内存,但是该内存一直无法释放;

    内存泄漏会导致内存溢出问题: 申请内存时,发现申请内存不足,就会报错 ;

    2.在介绍ThreadLocal内存泄漏问题之前,我们先说一下Java中的四种引用类型:强引用,软引用,弱引用和虚引用

    强引用: 当内存不足时,JVM 开始进行 GC(垃圾回收),对于强引用对象,就算是出现了 OOM 也不会对该对象进行回收,死都不回收。

    1. //定义一个强引用
    2. Object o1 = new Object();
    3. Object o2 = o1;
    4. o1 = null;
    5. System.gc();
    6. System.out.println(o1);//null
    7. System.out.println(o2);//不为空 java.lang.Object@1540e19d

     o1=null;表示o1不再指向堆内存空间,但是o2还是指向堆内存空间的

    软引用:当系统内存充足的时候,不会被回收;当系统内存不足时,它会被回收,软引用通 常用在对内存敏感的 程序中,比如高速缓存就用到软引用,内存够用时就保留,不够时就 回收。

    1. Object object2 = new Object();
    2. SoftReference<Object> objectSoftReference = new SoftReference<>(object2);
    3. object2 = null;
    4. try {
    5. //申请30M的堆内存
    6. byte[] bits = new byte[30 * 1024 * 1024];
    7. }catch (Exception e){
    8. }finally {
    9. System.out.println(object2);//null
    10. System.out.println(objectSoftReference.get());//java.lang.Object@1540e19d
    11. }

    当堆内存空间足够时,不会回收软引用对象objectSoftReference。

    下面我们设置jvm运行参数,配置参数设置最大堆内存大小

    -Xms5m -Xmx5m -XX:+PrintGCDetails

     我们再运行发现objectSoftReference软引用对象被回收了

     弱引用:弱引用需要用到 java.lang.ref.WeakReference 类来实现,它比软引用的生存周期更短。 对于只有弱引用的对象来说,只要有垃圾回收,不管 JVM 的内存空间够不够用,都会回收 该对象占用的内存空间。

    1. Object object3 = new Object();
    2. WeakReference<Object> objectWeakReference = new WeakReference<>(object3);
    3. object3 = null;
    4. System.gc();//执行垃圾回收 objectWeakReference对象会被回收掉
    5. System.out.println(object3);//null
    6. System.out.println(objectWeakReference.get());//null

    虚:虚引用需要 java.lang.ref.Phantomreference 类来实现。顾名思义,虚引用就是形同虚设。 与其它几种引用不同,虚引用并不会决定对象的生命周期,我们不必深究。

    3.ThreadLocal导致内存泄漏的原理

    首页我们写一下ThreadLocal的用法

    1. ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    2. stringThreadLocal.set("test");
    3. System.out.println(stringThreadLocal.get());//test

    ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。 Threadlocal 适用于在多线程的情况下,可以实现上下游的数据传递,实现线程隔离。 

    然后我们debug跟一下set方法的源码

    1. public void set(T value) {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null)
    5. map.set(this, value);
    6. else
    7. createMap(t, value);
    8. }

    底层其实就是一个ThreadLocalMap对象,和当前线程对象绑定,我们再断点跟一下map.set(this,value)

     Entry对象存的是键和值映射关系,源码中定义了一个Entry数组,意思就是我们可以在一个线程中,定义很多个ThreadLocal对象

    1. ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    2. stringThreadLocal.set("test");
    3. System.out.println(stringThreadLocal.get());//test
    4. ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
    5. stringThreadLocal2.set("hello");
    6. System.out.println(stringThreadLocal2.get());//hello

    Entry数组中存的数据 用伪代码表示 即[,...]

    这时我们执行stringThreadLocal = null,试想stringThreadLocal指向的堆内存空间,会被jvm垃圾回收掉吗?答案是:不会被回收

     当我们执行stringThreadLocal = null时,变量stringThreadLocal不再指向堆内存,但Entry中的key是弱引用(见下方源码),所以如果当前线程一直存活,堆内存中的ThreadLocal就不会被清理,就会导致内存泄漏问题。即使线程执行结束,执行垃圾回收,Entry中key会为null,但value还是有值的

    1. static class Entry extends WeakReference> {
    2. /** The value associated with this ThreadLocal. */
    3. Object value;
    4. Entry(ThreadLocal k, Object v) {
    5. super(k);
    6. value = v;
    7. }
    8. }

    4.解决方案

    方案a. 自己调用 remove 方法将不要的数据移除,避免内存泄漏的问题。原理:

    1. ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    2. stringThreadLocal.set("test");
    3. stringThreadLocal.remove();
    4. stringThreadLocal=null;

    先执行remove方法,则Entry中的K不再引用ThreadLocal对象,源码如下

    1. public void remove() {
    2. ThreadLocalMap m = getMap(Thread.currentThread());
    3. if (m != null)
    4. m.remove(this);
    5. }

    然后再执行stringThreadLocal = null,stringThreadLocal也不再指向堆内存中的ThreadLocal对象,这样堆内存中的ThreadLocal对象就不被任何人引用了,JVM垃圾回收就会清理掉堆内存中的ThreadLocal对象

    方案b.我们每次执行set方法时,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!

    1. ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    2. stringThreadLocal.set("test");
    3. stringThreadLocal = null;
    4. ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
    5. stringThreadLocal2.set("hello");

    在执行stringThreadLocal2.set("hello");时,我们进入源码

     发现如果某个Entry的K不再指向堆内存中的ThreadLocal,会将该Entry移除掉

    总结:存在内存泄露的有两个地方:ThreadLocal、Entry中Value;最保险还是要注意要自己及时调用remove方法。

  • 相关阅读:
    【夜读】你和高手的差距,就在于处理问题的能力
    【Python3】【力扣题】268. 丢失的数字
    解决Huggingface被墙下载模型的问题
    .NET Conf China 2023分享-.NET应用国际化-AIGC智能翻译+代码生成
    【算法入门-Python】02_递归
    牛客网SQL160
    网络框架重构之路plain2.0(c++23 without module) 综述
    软设之安全防范体系
    【postgresql 基础入门】数据表的查询基本知识,条件过滤、单列多列排序、按页浏览数据、数据去重,得到你想要的数据
    设计模式 --- 适配器模式 Adapter Pattern
  • 原文地址:https://blog.csdn.net/jack__love/article/details/127455917