强软弱虚四种引用以及ThreadLocal源码解析

六、强软弱虚四种引用以及ThreadLocal源码

强软弱虚引用

强引用

当我们使用Object obj = new Object()创建一个对象时,指向这个对象的引用就称为强引用。只要这个引用还指向一个对象,那么指向的这个对象就不会被垃圾回收器回收。

package com.gouxiazhi.reference;

/**
 * 强引用
 * @author 赵帅
 * @date 2021/1/17
 */
public class ReferenceDemo1 {

    public static void main(String[] args) {
        // 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M

        // 先创建一个1M的字节数组,这是强引用
        byte[] content = new byte[1024 * 1024];
        // 再创建一个2M的字节数组,因为上面已经创建了1M了,强引用只要content还指向对象,就不会被垃圾回收器回收。因此会抛出内存溢出OOM异常。
        // 当打开下面这句代码时,content不再指向一个对象,那么上面创建的对象就会被垃圾回收器回收,就可以正常运行了。
//        content = null;

        byte[] bytes = new byte[2 * 1024 * 1024];
    }
}

软引用

使用软引用创建的对象,当内存不够时,就会被垃圾回收器回收。

package com.gouxiazhi.reference;

import java.lang.ref.SoftReference;

/**
 * 软引用
 * @author 赵帅
 * @date 2021/1/17
 */
public class ReferenceDemo2 {

    public static void main(String[] args) {
        // 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M

        // 使用软引用创建一个1M的数组。
        SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024]);

        System.out.println("reference = " + reference.get());
        // 上面使用软引用创建了一个1M的数组,下面再创建2M的数组时,因为堆内存空间不够,就会调用gc清理掉软引用的指向的对象。因此不会抛出异常。
        // 如果上面不使用软引用,而使用 byte[] a = new byte[1024*1024];就会抛出内存溢出异常。
        byte[] bytes = new byte[2 * 1024 * 1024];
        // 因为创建上面的对象内存不够,因此软引用指向的对象已经被回收
        System.out.println("reference = " + reference.get());

    }
}

弱引用

使用弱引用创建的对象,只要垃圾回收器看见,就会回收。

package com.gouxiazhi.reference;

import java.lang.ref.WeakReference;
import java.util.Arrays;

/**
 * @author 赵帅
 * @date 2021/1/17
 */
public class ReferenceDemo3 {
    public static void main(String[] args) {

        // 弱引用指向的对象,只要垃圾回收器看见就立马回收掉。
        WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024]);
        System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
        System.gc();
        System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
    }
}

虚引用

主要用来管理堆外内存等。

package com.gouxiazhi.reference;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

/**
 * @author 赵帅
 * @date 2021/1/17
 */
public class ReferenceDemo4 {

    public static class M{
        @Override
        protected void finalize() throws Throwable {
            System.out.println("对象被回收了");
            super.finalize();
        }
    }

    public static void main(String[] args) {
        ReferenceQueue<M> referenceQueue = new ReferenceQueue<>();
        // 虚引用的使用,除了指向一个对象,还需要指定一个引用队列。
        PhantomReference<M> reference = new PhantomReference<>(new M(), referenceQueue);
        System.out.println("reference = " + reference);
        // 虚引用指向的对象无法被获取到,弱引用被垃圾回收器看见就会被回收,虚引用比弱引用级别更低
        System.out.println("reference = " + reference.get()); // null

        // 虚引用一般都是指向一个堆外内存,因为垃圾回收器只能回收堆内存,无法管理堆外内存.
        // 如果使用java管理堆外内存。假设M代表着堆外内存,那么当虚引用被回收时,他会将自身放入referenceQueue引用队列,开启另一个线程监听这个队列,
        // 当这个队列取到内容时,就代表要回收这块堆外内存了,可以执行回收堆外内存操作

        new Thread(() -> {
            
            // 如果从队列中取到虚引用,那么就表示需要回收这个堆外内存了。
            Reference<? extends M> poll = referenceQueue.poll();
            if (poll != null) {
                System.out.println("虚引用对象被jvm回收了" + poll);
            }
        }).start();


        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        ArrayList<byte[]> list = new ArrayList<>();

        while (true) {
            list.add(new byte[4 * 1024]);
        }

    }
}

ThreadLocal源码

查看ThreadLocal的set方法源码:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到,在set方法中,首先获取当前线程。然后通过getMap(t)获取了一个ThreadLocalMap对象。然后将要保存的对象存入了这个Map中,key值就是ThreadLoacl对象本身,value值为要保存的数据。

然后我们再点开getMap方法:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

返回的是t.threadLocals属性值,而且这个值是ThreadLocalMap类型的。查看ThreadLocalMap类:

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

可以看到ThreadLocalMap中存放数据的Entry节点继承自WeakReference<?>,因此说ThreadLocal底层用的是弱引用,而且在存储时,Map的key作为弱引用,也就是ThreadLocal对象本身作为弱引用存放,值是强引用存放的。

查看ThreadLocalMap类的set方法:

					private void set(ThreadLocal<?> key, Object value) {

            // 获取存放数据的数组,底层数据结构
            Entry[] tab = table;
            // 获取数组的长度
            int len = tab.length;
            // 计算key要存放的下标
            int i = key.threadLocalHashCode & (len-1);

            // 从下标i开始遍历数组,如果下标i 有元素了,也就是hash冲突了,那么就往后插并将下标自增
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
              
              // 获取下标为i的entry元素
                ThreadLocal<?> k = e.get();
							// 如果此位置的key与要保存的key相同则替换值
                if (k == key) {
                    e.value = value;
                    return;
                }
								// 如果key是空的话,说明这个位置的key已经过期被回收,则替换值为新的值。
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // 如果下标i为空,说明这个位置是空的。插入这个位置
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

简单分析上面整个过程,整个threadlocal存放数据过程甬道图如下:

![image-20210118143336157](/Users/zhaoshuai/Library/Application Support/typora-user-images/image-20210118143336157.png)

当调用threadLocal的set方法时:

  1. 获取当前线程的threadLocals属性。
  2. 调用threadLocals属性的set方法。
    • 获取threadLocalMap的entry数组。
    • 计算当前threadLocal对象的下标。
    • 获取下标的entry,如果没有则新建一个entry,如果有的话,判断当前的key是否与要存入的key一致,如果一致,则替换值。如果key为空的话,则替换值。如果上面条件都不满足,则新建一个entry对象。

ThreadLocal造成的内存泄漏

因为ThreadLocal是将自身作为弱引用存放在ThreadLocalMap中的,因此当一个ThreadLocal对象的强引用消失时。那么这个key将会被回收,这时原来这个key对应的value值如果没有被移除的话,那么就永远无法被访问到了。而且因为这个value值是作为entry节点的value引用指向的,value引用是一个强引用,那么这时,这个value属性就永远无法被回收,也无法被访问,就会造成内存泄漏。

使用ThreadLocal如何避免内存泄漏?

使用ThreadLocal set了一个值以后,在这个线程结束之前,一定要调用remove方法移除存放的值。

原文地址:https://www.cnblogs.com/Zs-book1/p/14292904.html