JVM - 關於垃圾收集的常見問題

聲明:java

本文由Yasin Shaw原創,首發於我的網站yasinshaw.com和公衆號"xy的技術圈"。算法

若是須要轉載請聯繫我(微信:yasinshaw)並在文章開頭顯著的地方註明出處數組

關注公衆號便可獲取學習資源或加入技術交流羣緩存

引用

Java有哪四種引用?分別介紹一下?

在JDK 1.2 以後,對引用的概念進行了擴充,把引用分爲強引用、軟引用、弱引用、虛引用。安全

  • 強引用:通常的引用,只要強引用還存在,垃圾收集器就不會回收這個對象;
  • 軟引用SoftReference:描述一些還有用但不是必須的對象。在系統將要發生OOM以前,會把這些對象列進回收範圍之中進行第二次回收,若是第二次回收後仍是沒有足夠的內存,纔會拋OOM異常;
  • 弱引用WeakReference:只能生存到下一次垃圾收集發生以前
  • 虛引用PhantomReference:一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。

這四種引用的應用場景是什麼?

強引用就不說了,咱們平時用得最多的就是強引用。微信

軟引用通常用於圖片緩存、網頁緩存或其它形式的緩存框架中。「內存緩存」中的圖片是以這種引用保存,使得 JVM 在發生 OOM 以前,能夠回收這部分緩存。多線程

Browser prev = new Browser();               // 獲取頁面進行瀏覽
SoftReference sr = new SoftReference(prev); // 瀏覽完畢後置爲軟引用 
if(sr.get() != null) { 
    rev = (Browser) sr.get();           // 尚未被回收器回收,直接獲取
} else {
    prev = new Browser();               // 因爲內存吃緊,因此對軟引用的對象回收了
    sr = new SoftReference(prev);       // 從新構建
}
複製代碼

JDK中的WeakHashMapThreadLocal使用了弱引用。併發

設計WeakHashMap類是爲了解決一個有趣的問題:若是有一個值,對應的鍵已經再也不使用了,將會出現什麼狀況呢?框架

假定對某個鍵的最後一次引用已經消亡,再也不有任何途徑引用這個值的對象了。可是,因爲在程序中的任何部分沒有再出現這個鍵,因此,這個鍵值對沒法從Map中刪除。學習

WeakHashMap 使用弱引用(weak references) 保存鍵。 WeakReference對象將引用保存到另一個對象中,在這裏,就是散列鍵。對於這種類型的對象,垃圾回收器用一種特有的方式進行處理。

一般,若是垃圾回收器發現某個特定的對象已經沒有他人引用了,就將其回收。然而,若是某個對象只能由 WeakReference引用,垃圾回收器仍然回收它,但要將引用這個對象的弱引用放人隊列中。

WeakHashMap將週期性地檢查隊列,以便找出新添加的弱引用。一個弱引用進人隊列意味着這個鍵再也不被他人使用,而且已經被收集起來。因而,WeakHashMap將刪除對應的鍵值對。

JDK中直接內存的回收就用到虛引用,因爲JVM自動內存管理的範圍是堆內存,而直接內存是在堆內存以外(實際上是內存映射文件),因此直接內存的分配和回收都是由Unsafe類去操做。

Java在申請一塊直接內存以後,會在堆內存分配一個對象保存這個堆外內存的引用,這個對象被垃圾收集器管理,一旦這個對象被回收,相應的用戶線程會收到通知並對直接內存進行清理工做。

關於軟引用的緩存功能,還有更好的替代方法嗎?

有的,咱們可使用最近最少使用算法(LRU)。其核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高」。最多見的實現是使用一個鏈表保存緩存數據,詳細算法實現以下:

  1. 新數據插入到鏈表頭部;
  2. 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
  3. 當鏈表滿的時候,將鏈表尾部的數據丟棄。

Redis的鍵過時策略就使用了LRU算法。

安全點

什麼是安全點和安全區域?

指的是程序在某個點或者某個區域是GC安全的。

對於Safe Point,另外一個須要考慮的問題是如何在GC發生時讓全部線程(這裏不包括執行JNI調用的線程)都「跑」到最近的安全點上再停頓下來。

這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension),其中搶先式中斷不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷, 若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件。

而主動式中斷的思想是當GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。

使用Safe Point彷佛已經完美地解決了如何進入GC的問題,但實際狀況卻並不必定。Safe Point機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safe Point。

可是,程序「不執行」的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程沒法響應JVM的中斷請求,「走」到安全的地方去中斷掛起,JVM也顯然不太可能等待線程從新被分配CPU時間。對於這種狀況,就須要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片斷之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。咱們也能夠把Safe Region看作是被擴展了的Safe Point。

在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待直到收到能夠安全離開Safe Region的信號爲止。

垃圾收集器

說一下常見的一些垃圾收集器?

新生代: Serial:單線程,新生代; ParNew: 多線程,新生代; Parallel Scavenge:多線程,新生代,關注吞吐量,容許較長的STW(Stop the world)換取吞吐量最大化; 老年代: Serial Old: 單線程,Serial的老年代版本; Parallel Old:多線程,Parallel Scavenge的老年代版本,關注吞吐量; CMS:多線程,標記-清除算法,關注停頓時間,能夠與Serial和ParNew配合。 其它: G1:同時負責新生代和老年代,是目前一段時間主流的垃圾收集器(JDK 9 到 11 的默認垃圾收集器)。 ZGC:在大堆下也能夠控制STW時間極短(幾毫秒內),在JDK 11 爲實驗階段。

Java 各版本模式默認是用的什麼垃圾收集器?

這裏只討論Server模式.

在JDK7,默認是Parallel Scavenge + Serial Old。

在JDK 8 及JDK 7u4以後的版本,默認是Parallel Scavenge + Parallel Old。

在JDK 9 到JDK 11,默認是G1

GC

介紹一下CMS老年代GC的幾個階段,並對它們的耗時作一下排序?

四個階段:

  1. 初始標記:標記GC Roots能直接關聯到的對象,須要STW(Stop The World);
  2. 併發標記:根據初始標記的對象作可達性分析;
  3. 從新標記:修正併發標記期間由於用戶應用程序繼續運做而致使標記產生變更的對象,須要STW;
  4. 併發清除:回收對象;

耗時排序:併發清除> 併發標記> 從新標記 > 初始標記

說一下CMS的CMF(Concurrent Mode Failure)產生的緣由?

CMS在老年代達到閾值(默認92%,能夠經過參數調整閾值)的時候,會進行Full GC。

若是在「併發清除」階段,因爲程序繼續運行,產生了過多的垃圾,預留的內存沒法知足程序須要,就會出現Concurrent Mode Failure。

說一下若是CMS發生了CMF,虛擬機是怎麼處理的?

若是出現了CMF,這時虛擬機將啓動後備預案: 臨時啓用Serial Old收集器來從新進行Full GC,這樣停頓時間就很長了。

G1從Java9開始成爲默認的垃圾收集器,它的優勢是什麼?

G1一直在持續不斷地改進中,G1設計的目的是替換CMS,它最主要的優勢是創建了「可預測的停頓模型」,它能夠儘可能去知足用戶指望的停頓時間。

同時,G1解決了CMS碎片化太多的問題。JVM系列下一篇文章咱們將重點討論G1。

內存分配和回收策略

對象在堆上的內存分配和回收策略?

  • 優先在Eden區分配。新生代通常分爲一個Eden加兩個Survivor,默認比例8:1:1(G1有所不一樣,下篇文章介紹)。
  • 大對象直接進入老年代。由於新生代通常使用複製算法,若是對象太大,在新生代複製的代價很是高,並且佔內存。常見的大對象有:很大的字符串、很大的byte數組(好比圖片等轉成的字節數組)。
  • 長期存活的對象進入老年代。對象頭有一個字段存放了對象的「年齡」信息,對象在新生代每通過一次Minor GC,就會+1,當它的年齡達到必定值(默認是15),就會進入老年代。
  • 動態對象年齡斷定:對象年齡不必定要達到閾值才進入老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該值的對象,就能夠直接進入老年代。
  • 空間分配擔保:在發生Minor GC以前,JVM會檢查老年代最大可用連續空間時候大於歷次晉升到老年代對象的平均大小,若是大於,就容許分配擔保,若是小於,就要進行一次Full GC。而若是容許了分配擔保,結果發現裝不下,就說明擔保失敗,那也會發起一次Full GC。

相關文章
相關標籤/搜索