写点什么

从垃圾回收的角度解析 ThreadLocal

作者:喝水不抬头
  • 2025-04-10
    上海
  • 本文字数:2127 字

    阅读完需:约 7 分钟

Java 中的三类引用

在正式介绍前,先介绍下 Java 中的三种类型的引用:强引用、软引用和弱引用,不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。

强引用

Object obj = new Object(); // 强引用obj = null; // 取消强引用,此时对象可被GC回收
复制代码

在上述代码里:

  • Object obj = new Object(); 这行代码创建了一个 Object 实例,并且变量 obj 对该实例持有强引用。只要 obj 变量没有被重新赋值或者置为 null,这个 Object 实例就不会被垃圾回收。

  • obj = null; 这行代码将 obj 变量置为 null,意味着切断了对 Object 实例的强引用。此时,该 Object 实例不再有强引用指向它,垃圾回收器在后续的垃圾回收过程中,会在合适的时机回收这个对象所占用的内存。

综上所述,在 Object obj = new Object(); 中,强引用就是变量 obj 对新创建的 Object 实例的引用,它确保了该对象在被引用期间不会被垃圾回收。


软引用

软引用是一种介于强引用和弱引用之间的引用类型。当一个对象仅被软引用所引用时,在系统内存充足的情况下,该对象不会被垃圾回收;但当系统内存不足时,垃圾回收器会回收这些仅被软引用的对象。

// VM参数:-XX:+UseG1GC -Xms10m -Xmx10mpublic class SoftReferenceExample {    public static void main(String[] args) {        // 创建一个软引用,指向一个 10MB 的字节数组        SoftReference<byte[]> softRef = new SoftReference<>(new byte[5 * 1024 * 1024]); // 5MB
// 第一次检查软引用对象 System.out.println("GC 前,软引用对象是否存在: " + (softRef.get() != null));
// 尝试触发垃圾回收(不一定会立即执行) System.gc();
// 短暂等待,给 GC 时间(仅用于演示,实际中不可靠) try { Thread.sleep(500); } catch (InterruptedException e) {}
// 第二次检查软引用对象(内存充足时可能未被回收) System.out.println("GC 后(内存充足),软引用对象是否存在: " + (softRef.get() != null));
// 强制分配大量内存,触发内存不足 List<byte[]> memoryHog = new ArrayList<>(); try { while (true) { memoryHog.add(new byte[10 * 1024 * 1024]); // 持续分配 10MB 的块 } } catch (OutOfMemoryError e) { System.out.println("内存不足,触发 OutOfMemoryError"); }
// 最后一次检查软引用对象 System.out.println("内存不足后,软引用对象是否存在: " + (softRef.get() != null)); }}
复制代码


弱引用

当一个对象仅被弱引用所引用时,在垃圾回收器执行垃圾回收操作时,无论当前内存是否充足,该对象都会被回收。

public class WeakReferenceWithQueueExample {    public static void main(String[] args) throws InterruptedException {        ReferenceQueue<Object> queue = new ReferenceQueue<>();        Object strongRef = new Object();        WeakReference<Object> weakRef = new WeakReference<>(strongRef, queue);
// 切断强引用, 当对象的强引用全部消失(如strongRef = null),且仅被弱引用引用时, // 下次垃圾回收会回收该对象,弱引用的 get() 方法返回 null strongRef = null; System.gc(); Thread.sleep(100);
// 当弱引用对象被回收时,该弱引用会被加入引用队列,通过队列可可靠地检测回收事件 Reference<? extends Object> poll = queue.poll(); if (poll != null) { System.out.println("弱引用对象已被回收"); } else { System.out.println("弱引用对象未被回收"); } }}
复制代码

ThreadLocalMap 为何容易发生内存泄漏

ThreadLocal 的内部结构

每个线程内部维护了一个 ThreadLocalMap,它是一个定制化的哈希表,用于存储线程本地变量:

class Thread {    ThreadLocal.ThreadLocalMap threadLocals = null; // 存储ThreadLocal变量}
复制代码

ThreadLocalMap 中的每个条目(Entry)是一个键值对,其中:

键(Key):弱引用(WeakReference)指向 ThreadLocal 对象。

值(Value):强引用指向实际存储的数据。

static class Entry extends WeakReference<ThreadLocal<?>> {    Object value; // 强引用    Entry(ThreadLocal<?> k, Object v) {        super(k); // Key是弱引用(继承自WeakReference)        value = v;    }}
复制代码

内存泄漏的触发条件

ThreadLocalMap 在 set/get 时会清理相邻的过期 Entry(启发式清理),但无法保证彻底清理。当以下两个条件同时满足时,会发生内存泄漏:

  • 条件 1:ThreadLocal 对象被回收

如果 ThreadLocal 实例(键)不再被其他强引用持有(例如置为 null 或超出作用域),由于 Entry 的键是弱引用,它会在下一次 GC 时被回收,此时 Entry 的 key 变为 null。

  • 条件 2:线程长期存活且未清理 Entry

如果线程是线程池中的核心线程或长期存活(例如 Web 容器的请求处理线程),ThreadLocalMap 会一直存在。即使 Entry 的 key 被回收,value 仍然被强引用持有,导致 value 无法被 GC 回收。

用户头像

还未添加个人签名 2018-05-15 加入

还未添加个人简介

评论

发布
暂无评论
从垃圾回收的角度解析ThreadLocal_喝水不抬头_InfoQ写作社区