java中強軟弱虛引用的妙用

前言

ThreadLocal 在什麼情況下可能發生內存泄漏?如果你想清楚這個問題的來龍去脈,看源碼是必不可少的,看了源碼之後你發現, ThreadLocal 中用到 static class Entry extends WeakReference<ThreadLocal<?>> {} ,謎底實際就是使用了弱引用 WeakReference

本文內容概要

  • 強引用:Object o = new Object()
  • 軟引用:new SoftReference(o);
  • 弱引用:new WeakReference(o);
  • 虛引用:new PhantomReference(o);
  • ThreadLocal 的使用,及使用不當發生內存泄漏的原因

Jdk 1.2 增加了抽象類 ReferenceSoftReferenceWeakReferencePhantomReference,擴展了引用類型分類,達到對內存更細粒度的控制。

比如我們的緩存數據,當內存不夠用的時候,我希望緩存可以釋放內存,或者將緩存存入到堆外等。

但我們怎麼區分哪些對象需要回收(垃圾回收算法,可達性分析),回收的時機,回收的時候可以讓我們拿到回收的通知,所以 JDK 1.2 帶來這幾個引用類型。

引用類型 什麼時候回收  
強引用 強引用的對象,只要 GC root 可達,不會被回收,內存不夠用了,會拋出 oom  
軟引用:SoftReference 軟引用對象,GC root 中,只有軟引用可以到達某個對象 a,在 oom 之前,垃圾回收會回收對象 a  
弱引用:WeakReference 弱引用,GC root 中,只有弱引用可以到達某個對象 c,發生 gc 就會被回收掉 c  
虛引用:PhantomReference 虛引用,必須配合 ReferenceQueue 使用,什麼時候回收不知道,但回收之後,可以操作 ReferenceQueue 獲取被回收的引用  

強引用

強引用就是我們經常用到的方式:Object o = new Object()。垃圾回收時,強引用的變量是不會被回收,只有設置 o=null,jvm 通過可達性分析,沒有 GC root 到達對象,垃圾回收器纔會清理堆中的對象,釋放內存。 當繼續申請內存分配,就會 oom。

定義一個類 Demo,Demo 實例佔用內存大小爲 10m,不停往 list 添加 Demo 的示例,由於不能申請到內存分配,程序拋出 oom 終止

// -Xmx600m
public class SoftReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(100);
            // 獲取 jvm 空閒的內存爲多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                objects.add(demo);
                count++;
                demo = null;
            }
            System.out.println("jvm 空閒內存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

以上代碼運行結果,拋出 oom 程序停止

jvm 空閒內存41 m
54
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.fly.blog.ref.SoftReferenceDemo$Demo.<init>(SoftReferenceDemo.java:37)
	at com.fly.blog.ref.SoftReferenceDemo.main(SoftReferenceDemo.java:25)

但是有的業務場景,需要我們在內存不夠用,可以釋放掉一些不必要的數據。比如我們在緩存中存的用戶信息。

軟引用

jdk 從 1.2 開始加入了 Reference ,SoftReference 是其中一個分類,它的作用是,通過 GC root 到達對象 a,僅有 SoftReference ,對象 a 將會在jvm oom 之前,被 jvm gc 釋放掉。

無限循環往 List 添加 10m 左右大小的數據(SoftReference),發現沒有出現 oom。

// -Xmx600m
public class SoftReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(500);
            // 獲取 jvm 空閒的內存爲多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                SoftReference<Demo> demoSoftReference = new SoftReference<>(demo);
                objects.add(demoSoftReference);
                count++;
                // demo 爲 null,只有 demoSoftReference 一條引用到達 Demo 的實例,GC 將會在 oom 之前回收 Demo 的實例
                demo = null;
            }
            System.out.println("jvm 空閒內存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }
    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

 

image-20200625213429845

 

 

通過 jvisualvm 查看 jvm 堆的使用,可以看到堆在要溢出的時候就會回收掉,空閒的內存很大的時候,你主動執行 執行垃圾回收,內存是不會回收的。

弱引用

對象 demo 的引用只有 WeakReference 可達時,會在 gc 之後回收 demo 釋放掉內存。

以下程序也會一直不停的運行,只是內存釋放的時機不同而已

// -Xmx600m -XX:+PrintGCDetails
public class WeakReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;

    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        while (true) {
            Thread.sleep(100);
            // 獲取 jvm 空閒的內存爲多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) >= 0) {
                Demo demo = new Demo(count);
                WeakReference<Demo> demoWeakReference = new WeakReference<>(demo);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm 空閒內存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;
        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

運行結果,SoftReference 可用內存在快用盡的時候就會釋放掉內存,而 WeakReference 每次可用內存達到 360m 左右會進行垃圾,而釋放掉內存

[GC (Allocation Failure) [PSYoungGen: 129159K->1088K(153088K)] 129175K->1104K(502784K), 0.0007990 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
jvm 空閒內存364 m
36
jvm 空閒內存477 m

虛引用

也有稱呼爲 幻靈引用,因爲你不知道什麼時候被回收,所需必須配合 ReferenceQueue,當對象回收時,可以從這個隊列拿到 PhantomReference 的實例。

// -Xmx600m -XX:+PrintGCDetails
public class PhantomReferenceDemo {
    // 1m
    private static int _1M = 1024 * 1024 * 1;

    private static ReferenceQueue referenceQueue = new ReferenceQueue();

    public static void main(String[] args) throws InterruptedException {
        ArrayList<Object> objects = Lists.newArrayListWithCapacity(50);
        int count = 1;
        new Thread(() -> {
            while (true) {
                try {
                    Reference remove = referenceQueue.remove();
                    // objects 可達性分析,可以到達 PhantomReference<Demo>,內存是不能及時釋放的,我們需要在隊裏中拿到那個 Demo 被回收了,然後
                    // 從 objects 移除這個對象
                    if (objects.remove(remove)) {
                        System.out.println("移除元素");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        while (true) {
            Thread.sleep(500);
            // 獲取 jvm 空閒的內存爲多少 m
            long meme_free = Runtime.getRuntime().freeMemory() / _1M;
            if ((meme_free - 10) > 40) {
                Demo demo = new Demo(count);
                PhantomReference<Demo> demoWeakReference = new PhantomReference<>(demo, referenceQueue);
                objects.add(demoWeakReference);
                count++;
                demo = null;
            }
            System.out.println("jvm 空閒內存" + meme_free + " m");
            System.out.println(objects.size());
        }
    }

    @Data
    static class Demo {
        private byte[] a = new byte[_1M * 10];
        private String str;

        public Demo(int i) {
            this.str = String.valueOf(i);
        }
    }
}

ThreadLocal

ThreadLocal 在我們實際開發中,用的還是比較多的。那它到底是個什麼東東呢(線程本地變量),我們知道 局部變量 (方法內定義的變量)和 成員變量 (類的屬性)。

有的時候呢,我們希望一個變量的生命週期可以貫穿整個線程的一個任務運行週期(線程池中的線程可以分配執行不同的任務),在各個方法調用的時候我們可以拿到這個預先設置的變量,這就是 ThreadLocal 的作用。

比如我們想要拿到當前請求的 HttpServletRequest,然後在當前各個方法都可以獲取到,SpringBoot 已經幫我們封裝好了,RequestContextFilter 在每個請求過來之後,都會通過 RequestContextHolder 設置線程本地變量,原理就是操作 ThreadLocal

ThreadLocal 只是針對當前線程中的調用,跨線程調用是不行的,所以 Jdk 通過 InheritableThreadLocal 繼承 ThreadLocal 來實現。

ThreadLocal 獲取當前請求的用戶信息

看註釋大致就能明白 TheadLocal 怎麼使用了

/**
 * @author 張攀欽
 * @date 2018/12/21-22:59
 */
@RestController
public class UserInfoController {
    @RequestMapping("/user/info")
    public UserInfoDTO getUserInfoDTO() {
        return UserInfoInterceptor.getCurrentRequestUserInfoDTO();
    }
}

@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
    private static final ThreadLocal<UserInfoDTO> THREAD_LOCAL = new ThreadLocal();
    // 請求頭用戶名
    private static final String USER_NAME = "userName";
    // 注意這個,只有注入到 ioc 中的 bean,才能注入進來
    @Autowired
    private IUserInfoService userInfoService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判斷是不是接口請求
        if (handler instanceof HandlerMethod) {
            String userName = request.getHeader(USER_NAME);
            UserInfoDTO userInfoByUserName = userInfoService.getUserInfoByUserName(userName);
            THREAD_LOCAL.set(userInfoByUserName);
            return true;
        }
        return false;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 用完之後記得釋放掉內存
        THREAD_LOCAL.remove();
    }
    // 獲取當前線程設置的用戶信息
    public static UserInfoDTO getCurrentRequestUserInfoDTO() {
        return THREAD_LOCAL.get();
    }
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 將 UserInfoInterceptor 注入到 ioc 容器中
     */
    @Bean
    public UserInfoInterceptor getUserInfoInterceptor() {
        return new UserInfoInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 調用這個方法返回的就是 ioc 的 bean
        registry.addInterceptor(getUserInfoInterceptor()).addPathPatterns("/**");
    }
}

InheritableThreadLocal

有的時候,我們希望當前線程的局部變量的生命週期可以延伸到子線程 中,父線程設置的變量,在子線程拿到。 InheritableThreadLocal 就是提供了這個能力。

/**
 * @author 張攀欽
 * @date 2020-06-27-21:18
 */
public class InheritableThreadLocalDemo {
    static InheritableThreadLocal<String> INHERITABLE_THREAD_LOCAL = new InheritableThreadLocal();
    static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        INHERITABLE_THREAD_LOCAL.set("父線程中使用 InheritableThreadLocal 設置變量");
        THREAD_LOCAL.set("父線程中使用 ThreadLocal 設置變量");
        Thread thread = new Thread(
                () -> {
                    // 能拿到設置的變量
                    System.out.println("從 InheritableThreadLocal 拿父線程設置的變量: " + INHERITABLE_THREAD_LOCAL.get());
                    // 打印爲 null
                    System.out.println("從 ThreadLocal 拿父線程設置的變量: " + THREAD_LOCAL.get());
                }
        );
        thread.start();
        thread.join();
    }
}

ThreadLocal get 方法源碼分析

你可以理解 Thead 對象有個屬性 Map,它的 key 是 ThreadLoal 實例,獲取線程局部變量的源碼

public class ThreadLocal<T> {
    public T get() {
        // 獲取運行在那個線程中
        Thread t = Thread.currentThread();
        // 從 Thread 拿 Map 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 使用 ThreadLocal 實例從 Map 獲取值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 初始化 Map,並返回初始化值,默認爲 null,你可以定義方法,從這個方法加載初始化值
        return setInitialValue();
    }
}

InheritableThreadLocal 獲取父線程設置的數據分析

每個 Thread 還有一個 Map 屬性爲 inheritableThreadLocals,用於保存從父線程複製過來的 value 。

當初始化子線程的時候,它會將父線程的 Map (inheritableThreadLocals) 的值複製到自己的 Thead Map (inheritableThreadLocals)過來,每個線程維護自己的 inheritableThreadLocals, 所以子線程改不了父線程維護的數據,只是子線程可以獲得父線程設置的數據。

public class Thread{
    
	// 維護線程本地變量
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 維護可以子線程可以繼承的父線程的數據
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
   // 線程初始化
    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (inheritThreadLocals && parent.inheritableThreadLocals != null){
            // 將父線程的 inheritableThreadLocals 數據複製到子線程中去
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
    }
}

public class TheadLocal{
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        /// 創建自己線程的 Map,將父線程的值複製進去
        return new ThreadLocalMap(parentMap);
    }

    static class ThreadLocalMap {
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            // 遍歷父線程,將數據複製過來
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
    }
} 

demo 驗證,以上分析

 

image-20200627232351534

 

 

 

image-20200627225502636

 

 

內存泄漏原因

定義了一個 20 大小的線程池,執行 50 次任務,執行完之後,將 threadLocal 置爲 null,模擬內存泄漏的場景 。爲了排除干擾因素,我設置 jvm 參數爲 -Xms8g -Xmx8g -XX:+PrintGCDetails

public class ThreadLocalDemo {
    private static ExecutorService executorService = Executors.newFixedThreadPool(20);
    private static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            executorService.submit(() -> {
                try {
                    threadLocal.set(new Demo());
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (Objects.nonNull(threadLocal)) {
                        // 爲防止內存泄漏,當前線程用完,清除掉 value
//                        threadLocal.remove();
                    }
                }
            });
        }
        Thread.sleep(5000);
        threadLocal = null;
        while (true) {
            Thread.sleep(2000);
        }
    }
    @Data
    static class Demo {
        //
        private Demo[] demos = new Demo[1024 * 1024 * 5];
    }
}

運行程序,沒有打印 gc 日誌,說明沒有進行垃圾回收

 

image-20200628020439866

 

 

 

image-20200628020512394

 

 

Java VisualVM 中我們 執行垃圾回收,回收之後的內存分佈,這個 20 個 ThreadLocalDemo$Demo[] 是回收不了的,這就是內存泄漏。

 

image-20200628020811328

 

 

程序循環 50 次創建了 50 個 Demo ,程序運行期間是不會觸發垃圾回收(設置 jvm 參數保證的),所以 ThreadLocalDemo$Demo[] 存活的實例數爲 50

當我手動觸發了 GC,實例數降爲 20,並不是我們期望的 0,這就是程序發生了內存泄漏問題

爲什麼發生了內存泄漏呢?

因爲每個線程對應一個Thread,線程池大小爲 20 個。Thread 中有 ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 中有 Entry[] tables,k 爲弱引用。當我們將 threadLocal 置爲 null 的時候,GC ROOT 到 ThreadLocalDemo$Demo[] 引用鏈還是存在的,只是 k 回收掉了,value 依然存在的,tables 長度是不會變的,是不會被回收的。

 

image-20200628023936332

 

 

ThreadLocal 在setget 的時候,針對 k 爲 null 的情況做了優化,會將對應的 tables[i] 設置爲 null。這樣單個 Entry 就可以被回收了。但是我們將 ThreadLocal 置爲 null 之後,不能操作方法調用了。只能等到 Thread 再次調用別的 ThreadLocal 時操作 ThreadLocalMap 時根據條件判斷,進行 Map 的 rehash,將 k 爲 null 的 Entry 刪除掉。

上述問題解決也比較方便,線程使用完 線程局部變量,調用 remove 主動清除 Entry 就可以了。


作者:張攀欽
鏈接:https://juejin.im/post/5ef7985e6fb9a07e7e0421e8

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章