文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

内存泄露的原因找到了,罪魁祸首居然是Java TheadLocal

2024-12-14 04:52

关注

 

本文转载自微信公众号「爱笑的架构师」,作者雷架 。转载本文请联系爱笑的架构师公众号。

ThreadLocal使用不规范,师傅两行泪

组内来了一个实习生,看这小伙子春光满面、精神抖擞、头发微少,我心头一喜:绝对是个潜力股。于是我找经理申请亲自来带他,为了帮助小伙子快速成长,我给他分了一个需求,这不需求刚上线几天就出网上问题了😭后台监控服务发现内存一直在缓慢上升,初步怀疑是内存泄露。

把实习生的PR都找出来仔细review,果然发现问题了。由于公司内部代码是保密的,这里简单写一个demo还原场景(忽略代码风格问题)。

  1. public class ThreadPoolDemo { 
  2.     private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         for (int i = 0; i < 100; ++i) { 
  5.             poolExecutor.execute(new Runnable() { 
  6.                 @Override 
  7.                 public void run() { 
  8.                     ThreadLocal threadLocal = new ThreadLocal<>(); 
  9.                     threadLocal.set(new BigObject()); 
  10.                     // 其他业务代码 
  11.                 } 
  12.             }); 
  13.             Thread.sleep(1000); 
  14.         } 
  15.     } 
  16.     static class BigObject { 
  17.         // 100M 
  18.         private byte[] bytes = new byte[100 * 1024 * 1024]; 
  19.     } 

代码分析:

乍一看这代码好像没有什么问题,那为什么会导致服务GC后内存还高居不下呢?

代码中给threadLocal赋值了一个大的对象,但是执行完业务逻辑后没有调用remove方法,最后导致线程池中10个线程的threadLocals变量中包含的大对象没有被释放掉,出现了内存泄露。

大家说说这样的实习生还能留不?

ThreadLocal的value值存在哪里?

实习生说他以为线程任务结束了threadLocal赋值的对象会被JVM垃圾回收,很疑惑为什么会出现内存泄露。作为师傅我肯定要给他把原理讲透呀。

ThreadLocal类提供set/get方法存储和获取value值,但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类,ThreadLocalMap是ThreadLocal的一个静态内部类,它的key是ThreadLocal实例对象,value是任意Object对象。

ThreadLocalMap类的定义

  1. static class ThreadLocalMap { 
  2.     // 定义一个table数组,存储多个threadLocal对象及其value值 
  3.     private Entry[] table
  4.     ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { 
  5.         table = new Entry[INITIAL_CAPACITY]; 
  6.         int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 
  7.         table[i] = new Entry(firstKey, firstValue); 
  8.         size = 1; 
  9.         setThreshold(INITIAL_CAPACITY); 
  10.     } 
  11.     // 定义一个Entry类,key是一个弱引用的ThreadLocal对象 
  12.     // value是任意对象 
  13.     static class Entry extends WeakReference> { 
  14.          
  15.         Object value; 
  16.         Entry(ThreadLocal k, Object v) { 
  17.             super(k); 
  18.             value = v; 
  19.         } 
  20.     } 
  21.     // 省略其他 

进一步分析ThreadLocal类的代码,看set和get方法如何与ThreadLocalMap静态内部类关联上。

ThreadLocal类set方法

  1. public class ThreadLocal { 
  2.  public void set(T value) { 
  3.         Thread t = Thread.currentThread(); 
  4.         ThreadLocalMap map = getMap(t); 
  5.         if (map != null
  6.             map.set(this, value); 
  7.         else 
  8.             createMap(t, value); 
  9.     } 
  10.  
  11.     ThreadLocalMap getMap(Thread t) { 
  12.         return t.threadLocals; 
  13.     } 
  14.  
  15.     void createMap(Thread t, T firstValue) { 
  16.         t.threadLocals = new ThreadLocalMap(this, firstValue); 
  17.     } 
  18.     // 省略其他方法 

set的逻辑比较简单,就是获取当前线程的ThreadLocalMap,然后往map里添加KV,K是当前ThreadLocal实例,V是我们传入的value。这里需要注意一下,map的获取是需要从Thread类对象里面取,看一下Thread类的定义。

  1. public class Thread implements Runnable { 
  2.     ThreadLocal.ThreadLocalMap threadLocals = null
  3.     //省略其他 

Thread类维护了一个ThreadLocalMap的变量引用。

ThreadLocal类get方法

get获取当前线程的对应的私有变量,是之前set或者通过initialValue的值,代码如下:

  1. class ThreadLocal { 
  2.     public T get() { 
  3.         Thread t = Thread.currentThread(); 
  4.         ThreadLocalMap map = getMap(t); 
  5.         if (map != null) { 
  6.             ThreadLocalMap.Entry e = map.getEntry(this); 
  7.             if (e != null
  8.                 return (T)e.value; 
  9.         } 
  10.         return setInitialValue(); 
  11.     } 

代码逻辑分析:

ThreadLocal相关类的关系总结

看了上面的分析是不是对Thread,ThreadLocal,ThreadLocalMap,Entry这几个类之间的关系有点晕了,没关系我专门画了一个UML类图来总结(忽略UML标准语法)。

 

 


ThreadLocal相关类的关系

 

 

ThreadLocal内存模型原理

经过上面的分析我们对ThreadLocal相关的类设计已经非常清楚了,下面通过一张图更加深入理解一下ThreadLocal的内存存储。

 

 


ThreadLocal内存模型

 

 

图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。

强引用弱引用的概念

ThreadLocalMap的key是一个弱引用类型,源代码如下:

  1. static class ThreadLocalMap { 
  2.     // 定义一个Entry类,key是一个弱引用的ThreadLocal对象 
  3.     // value是任意对象 
  4.     static class Entry extends WeakReference> { 
  5.          
  6.         Object value; 
  7.         Entry(ThreadLocal k, Object v) { 
  8.             super(k); 
  9.             value = v; 
  10.         } 
  11.     } 
  12.     // 省略其他 

下面解释一下常见的几种引用概念。

强引用

一直活着:类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

弱引用

回收就会死亡:被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

软引用

有一次活的机会:软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

内存泄露是不是弱引用的锅?

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么ThreadLocalMap使用弱引用而不是强引用?

翻看官网文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.

为了处理非常大和长期的用途,哈希表条目使用weakreference作为键。

分两种情况讨论:

(1)key 使用强引用

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

(2)key 使用弱引

引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal最佳实践

通过前面几小节我们分析了ThreadLocal的类设计以及内存模型,同时也重点分析了发生内存泄露的条件和特定场景。最后结合项目中的经验给出建议使用ThreadLocal的场景:

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

答案就是:每次使用完ThreadLocal,建议调用它的remove()方法,清除数据。

另外需要强调的是并不是所有使用ThreadLocal的地方,都要在最后remove(),因为他们的生命周期可能是需要和项目的生存周期一样长的,所以要进行恰当的选择,以免出现业务逻辑错误!

来源:爱笑的架构师内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯