問題答案在文中都有提到html
當須要排查各類內存溢出問題、當垃圾收集成爲系統達到更高併發的瓶頸時,咱們就須要對這些「自動化」的技術實施必要的監控和調節。java
Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 堆 內存中對象的分配與回收。git
Java 堆是垃圾收集器管理的主要區域,所以也被稱做GC 堆(Garbage Collected Heap).從垃圾回收的角度,因爲如今收集器基本都採用分代垃圾收集算法,因此 Java 堆還能夠細分爲:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。程序員
堆空間的基本結構:github
上圖所示的 eden 區、s0("From") 區、s1("To") 區都屬於新生代,tentired 區屬於老年代。大部分狀況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,若是對象還存活,則會進入 s1("To"),而且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold
來設置。通過此次GC後,Eden區和"From"區已經被清空。這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的「From」,新的"From"就是上次GC前的"To"。無論怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到「To」區被填滿,"To"區被填滿以後,會將全部對象移動到年老代中。面試
目前主流的垃圾收集器都會採用分代回收算法,所以須要將堆內存分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。算法
大多數狀況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.下面咱們來進行實際測試如下。spring
在測試以前咱們先來看看 Minor GC 和 Full GC 有什麼不一樣呢?後端
測試:數組
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2;
allocation1 = new byte[30900*1024];
//allocation2 = new byte[900*1024];
}
}
複製代碼
經過如下方式運行:
添加的參數:-XX:+PrintGCDetails
運行結果 (紅色字體描述有誤,應該是對應於 JDK1.7 的永久代):
從上圖咱們能夠看出 eden 區內存幾乎已經被分配徹底(即便程序什麼也不作,新生代也會使用 2000 多 k 內存)。假如咱們再爲 allocation2 分配內存會出現什麼狀況呢?
allocation2 = new byte[900*1024];
複製代碼
簡單解釋一下爲何會出現這種狀況: 由於給 allocation2 分配內存的時候 eden 區內存幾乎已經被分配完了,咱們剛剛講了當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次 Minor GC.GC 期間虛擬機又發現 allocation1 沒法存入 Survivor 空間,因此只好經過 分配擔保機制 把新生代的對象提早轉移到老年代中去,老年代上的空間足夠存放 allocation1,因此不會出現 Full GC。執行 Minor GC 後,後面分配的對象若是可以存在 eden 區的話,仍是會在 eden 區分配內存。能夠執行以下代碼驗證:
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
allocation1 = new byte[32000*1024];
allocation2 = new byte[1000*1024];
allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
}
}
複製代碼
大對象就是須要大量連續內存空間的對象(好比:字符串、數組)。
爲何要這樣呢?
爲了不爲大對象分配內存時因爲分配擔保機制帶來的複製而下降效率。
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別哪些對象應放在新生代,哪些對象應放在老年代中。爲了作到這一點,虛擬機給每一個對象一個對象年齡(Age)計數器。
若是對象在 Eden 出生並通過第一次 Minor GC 後仍然可以存活,而且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並將對象年齡設爲 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增長 1 歲,當它的年齡增長到必定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,能夠經過參數 -XX:MaxTenuringThreshold
來設置。
爲了更好的適應不一樣程序的內存狀況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,若是 Survivor 空間中相同年齡全部對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無需達到要求的年齡。
堆中幾乎放着全部的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任什麼時候候計數器爲 0 的對象就是不可能再被使用的。
這個方法實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最主要的緣由是它很難解決對象之間相互循環引用的問題。 所謂對象之間的相互引用問題,以下面代碼所示:除了對象 objA 和 objB 相互引用着對方以外,這兩個對象之間再無任何引用。可是他們由於互相引用對方,致使它們的引用計數器都不爲 0,因而引用計數算法沒法通知 GC 回收器回收他們。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
複製代碼
這個算法的基本思想就是經過一系列的稱爲 「GC Roots」 的對象做爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證實此對象是不可用的。
不管是經過引用計數法判斷對象引用數量,仍是經過可達性分析法判斷對象的引用鏈是否可達,斷定對象的存活都與「引用」有關。
JDK1.2 以前,Java 中引用的定義很傳統:若是 reference 類型的數據存儲的數值表明的是另外一塊內存的起始地址,就稱這塊內存表明一個引用。
JDK1.2 之後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
1.強引用(StrongReference)
之前咱們使用的大部分引用實際上都是強引用,這是使用最廣泛的引用。若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。
2.軟引用(SoftReference)
若是一個對象只具備軟引用,那就相似於無關緊要的生活用品。若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
若是一個對象只具備弱引用,那就相似於無關緊要的生活用品。弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。
弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
4.虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序若是發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。
特別注意,在程序設計中通常不多使用弱引用與虛引用,使用軟引用的狀況較多,這是由於軟引用能夠加速 JVM 對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
即便在可達性分析法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑階段」,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過期,虛擬機將這兩種狀況視爲沒有必要執行。
被斷定爲須要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象創建關聯,不然就會被真的回收。
運行時常量池主要回收的是廢棄的常量。那麼,咱們如何判斷一個常量是廢棄常量呢?
假如在常量池中存在字符串 "abc",若是當前沒有任何 String 對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,若是這時發生內存回收的話並且有必要的話,"abc" 就會被系統清理出常量池。
注意:咱們在 多是把 Java 內存區域講的最清楚的一篇文章 也講了 JDK1.7 及以後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。
方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?
斷定一個常量是不是「廢棄常量」比較簡單,而要斷定一個類是不是「無用的類」的條件則相對苛刻許多。類須要同時知足下面 3 個條件才能算是 「無用的類」 :
虛擬機能夠對知足上述 3 個條件的無用類進行回收,這裏說的僅僅是「能夠」,而並非和對象同樣不使用了就會必然被回收。
該算法分爲「標記」和「清除」階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。它是最基礎的收集算法,後續的算法都是對其不足進行改進獲得。這種垃圾收集算法會帶來兩個明顯的問題:
爲了解決效率問題,「複製」收集算法出現了。它能夠將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另外一塊去,而後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
根據老年代的特色提出的一種標記算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。
當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存分爲幾塊。通常將 java 堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。
好比在新生代中,每次收集都會有大量對象死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇「標記-清除」或「標記-整理」算法進行垃圾收集。
延伸面試問題: HotSpot 爲何要分爲新生代和老年代?
根據上面的對分代收集算法的介紹回答。
若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。
雖然咱們對各個收集器進行比較,但並不是要挑選出一個最好的收集器。由於直到如今爲止尚未最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,咱們能作的就是根據具體應用場景選擇適合本身的垃圾收集器。試想一下:若是有一種四海以內、任何場景下都適用的完美收集器存在,那麼咱們的 HotSpot 虛擬機就不會實現那麼多不一樣的垃圾收集器了。
Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。你們看名字就知道這個收集器是一個單線程收集器了。它的 「單線程」 的意義不只僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工做,更重要的是它在進行垃圾收集工做的時候必須暫停其餘全部的工做線程( "Stop The World" ),直到它收集結束。
新生代採用複製算法,老年代採用標記-整理算法。
虛擬機的設計者們固然知道 Stop The World 帶來的不良用戶體驗,因此在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。
可是 Serial 收集器有沒有優於其餘垃圾收集器的地方呢?固然有,它簡單而高效(與其餘收集器的單線程相比)。Serial 收集器因爲沒有線程交互的開銷,天然能夠得到很高的單線程收集效率。Serial 收集器對於運行在 Client 模式下的虛擬機來講是個不錯的選擇。
ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其他行爲(控制參數、收集算法、回收策略等等)和 Serial 收集器徹底同樣。
新生代採用複製算法,老年代採用標記-整理算法。
它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的併發收集器,後面會介紹到)配合工做。
並行和併發概念補充:
並行(Parallel) :指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另外一個 CPU 上。
Parallel Scavenge 收集器也是使用複製算法的多線程收集器,它看上去幾乎和ParNew都同樣。 那麼它有什麼特別之處呢?
-XX:+UseParallelGC
使用 Parallel 收集器+ 老年代串行
-XX:+UseParallelOldGC
使用 Parallel 收集器+ 老年代並行
複製代碼
Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提升用戶體驗)。所謂吞吐量就是 CPU 中用於運行用戶代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了不少參數供用戶找到最合適的停頓時間或最大吞吐量,若是對於收集器運做不太瞭解的話,手工優化存在困難的話能夠選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
新生代採用複製算法,老年代採用標記-整理算法。
Serial 收集器的老年代版本,它一樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另外一種用途是做爲 CMS 收集器的後備方案。
Parallel Scavenge 收集器的老年代版本。使用多線程和「標記-整理」算法。在注重吞吐量以及 CPU 資源的場合,均可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它很是符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。
從名字中的Mark Sweep這兩個詞能夠看出,CMS 收集器是一種 「標記-清除」算法實現的,它的運做過程相比於前面幾種垃圾收集器來講更加複雜一些。整個過程分爲四個步驟:
從它的名字就能夠看出它是一款優秀的垃圾收集器,主要優勢:併發收集、低停頓。可是它有下面三個明顯的缺點:
G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高機率知足 GC 停頓時間要求的同時,還具有高吞吐量性能特徵.
被視爲 JDK1.7 中 HotSpot 虛擬機的一個重要進化特徵。它具有一下特色:
G1 收集器的運做大體分爲如下幾個步驟:
G1 收集器在後臺維護了一個優先列表,每次根據容許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃份內存空間以及有優先級的區域回收方式,保證了 GF 收集器在有限時間內能夠儘量高的收集效率(把內存化整爲零)。
做者的其餘開源項目推薦: