threadLocal为什么会导致内存泄露

  • 每个线程都有一个ThreadLocalMap, 该ThreadLocalMap 中有许多entry,每个entry的key就是当前的threadLocal的弱引用,value是填入的值
  • 当系统发生gc的时候,当没有地方强引用该threadLocal,那么这个弱引用的key就会被回收,
  • 但是这个entry仍旧被threadLocalMap强引用,threadLocalMap被当前线程强引用,因此无法回收,导致内存泄露
  • 所以每次用完threadLocal之后都需要去remove它,并且threadLocal在set,get,remove的时候会清除key为null的value值

ThreadLocal示例图:
threadLocal为什么会导致内存泄露_第1张图片

public class TestThreadLocal {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> test("abc", false));
        t.start();
        t.join();
        System.out.println("----gc后----");
        Thread t2 = new Thread(() -> test("def", true));
        t2.start();
        t2.join();
    }

    @SneakyThrows
    private static void test(String s, boolean isGc) {
        new ThreadLocal<>().set(s);
        if (isGc) {
            System.gc();
        }

        Thread t = Thread.currentThread();
        Class<? extends Thread> clz = t.getClass();
        Field field = clz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object threadLocalMap = field.get(t);
        Class<?> tlmClass = threadLocalMap.getClass();
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] arr = (Object[]) tableField.get(threadLocalMap);
        for (Object o : arr) {
            if (o != null) {
                Class<?> entryClass = o.getClass();
                Field valueField = entryClass.getDeclaredField("value");
                Field referentField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referentField.setAccessible(true);

                Object value = valueField.get(o);
                Object referent = referentField.get(o);
                System.out.println(String.format("弱引用:%s, 值:%s", referent, value));
            }
        }
    }
}
弱引用:java.lang.ThreadLocal@71442059,:abc
弱引用:java.lang.ThreadLocal@5dce77fa,:java.lang.ref.SoftReference@20789f2d
----gc后----
弱引用:null,:def

2.线程池中的线程被复用,threadLocal中的值也会被复用,所以每次用完线程之后都要手动remove该threadLocal

/**
 * 线程池中的线程被复用,threadLocal中的值也会被复用,所以每次用完线程之后都要手动remove该threadLocal
 * @author xuleyan
 * @version TestPool.java, v 0.1 2021-07-30 8:16 下午
 */
public class TestPool {

    public static ThreadLocal<Integer> valueHolder = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static int getValue() {
        return valueHolder.get();
    }

    public static void remove() {
        valueHolder.remove();
    }

    public static void increment() {
        valueHolder.set(valueHolder.get() + 1);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.execute(() -> {
                try {
                    long threadId = Thread.currentThread().getId();
                    int before = getValue();
                    increment();
                    int after = getValue();
                    System.out.println("threadId: " + threadId + ", before: " + before + ", after: " + after);
                } finally {
                    remove();
                }
            });
        }
        executorService.shutdown();
    }
}

1.finally中添加 remove()
threadId: 10, before: 0, after: 1
threadId: 11, before: 0, after: 1
threadId: 11, before: 0, after: 1
threadId: 10, before: 0, after: 1
threadId: 12, before: 0, after: 1

2.finally中不使用 remove()
threadId: 10, before: 0, after: 1
threadId: 11, before: 0, after: 1
threadId: 11, before: 1, after: 2
threadId: 10, before: 1, after: 2
threadId: 12, before: 0, after: 1

3.模拟threadLocal导致的内存溢出

// -Xms128m -Xmx128m -Xmn64M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 
// 打印gc信息,并设置内存限制快速内存溢出
public class TestThreadLocalOom {

    public static final Integer BIG_LOOP = 10000;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 500; i ++) {
            executorService.execute(() -> {
                ThreadLocal<List<User>> threadLocal = new ThreadLocal<>();
                threadLocal.set(new TestThreadLocalOom().addBigList());
                System.out.println("ThreadId:" + Thread.currentThread().getName() + "addBigList");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // threadLocal.remove();
            });
        }

        Thread.sleep(10000);
        executorService.shutdown();
    }

    public List<User> addBigList() {
        List<User> params = new ArrayList<>(BIG_LOOP);
        for (int i = 0; i<BIG_LOOP; i++) {
            params.add(new User("haha", 28));
        }
        return params;
    }
}

(1)注释掉remove(), 就会出现疯狂的full gc, 最终会出现oom

PSYoungGen: 49152K->49152K(57344K) 新生代100%
ParOldGen: 65320K->65279K(65536K)。 老年代100%
114472K->114431K(122880K)。         总的堆空间100%挤满
Metaspace: 6067K->6067K(1056768K)。 元空间100%

46.314: [Full GC (Ergonomics) java.lang.OutOfMemoryError: GC overhead limit exceeded
[PSYoungGen: 49152K->49152K(57344K)] [ParOldGen: 65320K->65279K(65536K)] 114472K->114431K(122880K), [Metaspace: 6067K->6067K(1056768K)], 0.1804484 secs] [Times: user=0.59 sys=0.01, real=0.18 secs] 
46.495: [Full GC (Ergonomics) [PSYoungGen: 49152K->49152K(57344K)] [ParOldGen: 65281K->65280K(65536K)] 114433K->114432K(122880K), [Metaspace: 6067K->6067K(1056768K)], 0.1546160 secs] [Times: user=0.49 sys=0.00, real=0.16 secs] 

(2)不注释remove()方法,几乎没有fullgc, 且堆空间占用不高,说明大部分线程中的threadLocalMap中的entry都被回收了。


Heap
 PSYoungGen      total 57344K, used 15858K [0x00000007bc000000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 49152K, 32% used [0x00000007bc000000,0x00000007bcf7cb40,0x00000007bf000000)
  from space 8192K, 0% used [0x00000007bf000000,0x00000007bf000000,0x00000007bf800000)
  to   space 8192K, 0% used [0x00000007bf800000,0x00000007bf800000,0x00000007c0000000)
 ParOldGen       total 65536K, used 8259K [0x00000007b8000000, 0x00000007bc000000, 0x00000007bc000000)
  object space 65536K, 12% used [0x00000007b8000000,0x00000007b8810f60,0x00000007bc000000)
 Metaspace       used 6136K, capacity 6382K, committed 6400K, reserved 1056768K
  class space    used 684K, capacity 765K, committed 768K, reserved 1048576K

你可能感兴趣的:(java,多线程,java,内存泄漏)