天天看點

強軟弱虛四種引用以及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方法移除存放的值。