摘要:之前的堆內存示意圖從上圖可以看出堆內存的分為新生代老年代和永久代。對象優先在區分配目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內存分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
上文回顧:《可能是把Java內存區域講的最清楚的一篇文章》
寫在前面 本節常見面試題:問題答案在文中都有提到
如何判斷對象是否死亡(兩種方法)。
簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。
如何判斷一個常量是廢棄常量
如何判斷一個類是無用的類
垃圾收集有哪些算法,各自的特點?
HotSpot為什么要分為新生代和老年代?
常見的垃圾回收器有那些?
介紹一下CMS,G1收集器。
Minor Gc和Full GC 有什么不同呢?
本文導火索當需要排查各種 內存溢出問題、當垃圾收集稱為系統達到更高并發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。
Java程序員必看文檔哈哈 皮一下!我自己開源的一個Java學習指南文檔。一份涵蓋大部分Java程序員所需要掌握的核心知識,正在一步一步慢慢完善,期待您的參與。Github地址:https://github.com/Snailclimb/Java-Guide 。看一眼,我覺得你一定不會后悔,如果可以的話,可以給個Star鼓勵一下!
1 揭開JVM內存分配與回收的神秘面紗Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 堆 內存中對象的分配與回收。
JDK1.8之前的堆內存示意圖:
從上圖可以看出堆內存的分為新生代、老年代和永久代。新生代又被進一步分為:Eden 區+Survior1 區+Survior2 區。值得注意的是,在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。
1.1 對象優先在eden區分配目前主流的垃圾收集器都會采用分代回收算法,因此需要將堆內存分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
大多數情況下,對象在新生代中 eden 區分配。當 eden 區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC.下面我們來進行實際測試以下。
在測試之前我們先來看看 Minor Gc和Full GC 有什么不同呢?
新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。
老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(并非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。
測試:
public class GCTest { public static void main(String[] args) { byte[] allocation1, allocation2; allocation1 = new byte[30900*1024]; //allocation2 = new byte[900*1024]; } }
通過以下方式運行:
添加的參數:-XX:+PrintGCDetails
運行結果:
從上圖我們可以看出eden區內存幾乎已經被分配完全(即使程序什么也不做,新生代也會使用2000多k內存)。假如我們再為allocation2分配內存會出現什么情況呢?
allocation2 = new byte[900*1024];
簡單解釋一下為什么會出現這種情況: 因為給allocation2分配內存的時候eden區內存幾乎已經被分配完了,我們剛剛講了當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC.GC期間虛擬機又發現allocation1無法存入Survior空間,所以只好通過 分配擔保機制 把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放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]; } }1.2 大對象直接進入老年代
大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。
為什么要這樣呢?
為了避免為大對象分配內存時由于分配擔保機制帶來的復制而降低效率。
1.3 長期存活的對象將進入老年代既然虛擬機采用了分代收集的思想來管理內存,那么內存回收時就必須能識別那些對象應放在新生代,那些對象應放在老年代中。為了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。
如果對象在 Eden 出生并經過第一次 Minor GC 后仍然能夠存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并將對象年齡設為1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
1.4 動態對象年齡判定為了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需達到要求的年齡。
2 對象已經死亡?堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。
2.1 引用計數法給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加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; } }2.2 可達性分析算法
這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。
2.3 再談引用無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關。
JDK1.2之前,Java中引用的定義很傳統:如果reference類型的數據存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。
JDK1.2以后,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)
1.強引用
以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
2.軟引用(SoftReference)
如果一個對象只具有軟引用,那就類似于可有可物的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
3.弱引用(WeakReference)
如果一個對象只具有弱引用,那就類似于可有可物的生活用品。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。
弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
4.虛引用(PhantomReference)
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
虛引用主要用來跟蹤對象被垃圾回收的活動。
虛引用與軟引用和弱引用的一個區別在于: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。
2.4 不可達的對象并非“非死不可”即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。
被判定為需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。
2.5 如何判斷一個常量是廢棄常量運行時常量池主要回收的是廢棄的常量。那么,我們如何判斷一個常量是廢棄常量呢?
假如在常量池中存在字符串 "abc",如果當前沒有任何String對象引用該字符串常量的話,就說明常量 "abc" 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,"abc" 就會被系統清理出常量池。
注意:我們在 可能是把Java內存區域講的最清楚的一篇文章 也講了JDK1.7及之后版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開辟了一塊區域存放運行時常量池。
2.6 如何判斷一個類是無用的類方法區主要回收的是無用的類,那么如何判斷一個類是無用的類的呢?
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :
該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
加載該類的 ClassLoader 已經被回收。
該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。
3 垃圾收集算法 3.1 標記-清除算法算法分為“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。它是最基礎的收集算法,效率也很高,但是會帶來兩個明顯的問題:
效率問題
空間問題(標記清除后會產生大量不連續的碎片)
3.2 復制算法為了解決效率問題,“復制”收集算法出現了。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。
3.3 標記-整理算法根據老年代的特點特出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一段移動,然后直接清理掉端邊界以外的內存。
3.4 分代收集算法當前虛擬機的垃圾手機都采用分代收集算法,這種算法沒有什么新的思想,只是根據對象存活周期的不同將內存分為幾塊。一般將java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。
比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清楚”或“標記-整理”算法進行垃圾收集。
延伸面試問題: HotSpot為什么要分為新生代和老年代?
根據上面的對分代收集算法的介紹回答。
4 垃圾收集器如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現。
雖然我們對各個收集器進行比較,但并非了挑選出一個最好的收集器。因為知道現在位置還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那么我們的HotSpot虛擬機就不會實現那么多不同的垃圾收集器了。
4.1 Serial收集器Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束。
新生代采用復制算法,老年代采用標記-整理算法。
虛擬機的設計者們當然知道Stop The World帶來的不良用戶體驗,所以在后續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。
但是Serial收集器有沒有優于其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial收集器對于運行在Client模式下的虛擬機來說是個不錯的選擇。
4.2 ParNew收集器ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。
新生代采用復制算法,老年代采用標記-整理算法。
它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,只有它能與CMS收集器(真正意義上的并發收集器,后面會介紹到)配合工作。
并行和并發概念補充:
并行(Parallel) :指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態。
并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個CPU上。
4.3 Parallel Scavenge收集器Parallel Scavenge 收集器類似于ParNew 收集器。 那么它有什么特別之處呢?
-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行 -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用于運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解的話,手工優化存在的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
新生代采用復制算法,老年代采用標記-整理算法。
Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的后備方案。
4.5 Parallel Old收集器Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。
4.6 CMS收集器CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它而非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
初始標記: 暫停所有的其他線程,并記錄下直接與root相連的對象,速度很快 ;
并發標記: 同時開啟GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發生引用更新的地方。
重新標記: 重新標記階段就是為了修正并發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發標記階段時間短
并發清除: 開啟用戶線程,同時GC線程開始對為標記的區域做清掃。
從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:并發收集、低停頓。但是它有下面三個明顯的缺點:
對CPU資源敏感;
無法處理浮動垃圾;
它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
4.7 G1收集器G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征.
被視為JDK1.7中HotSpot虛擬機的一個重要進化特征。它具備一下特點:
并行與并發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓java程序繼續執行。
分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
空間整合:與CMS的“標記--清理”算法不同,G1從整體來看是基于“標記整理”算法實現的收集器;從局部上來看是基于“復制”算法實現的。
可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內。
G1收集器的運作大致分為以下幾個步驟:
初始標記
并發標記
最終標記
篩選回收
G1收集器在后臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。
參考:
《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版》
https://my.oschina.net/hosee/...
你若盛開,清風自來。 歡迎關注我的微信公眾號:“Java面試通關手冊”,一個有溫度的微信公眾號。公眾號有大量資料,回復關鍵字“1”你可能看到想要的東西哦!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76890.html
摘要:在這種消耗很高的狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。 Java 垃圾回收(GC) 泛讀 文章地址: https://segmentfault.com/a/1190000008922319 0. 序言 帶著問題去看待 垃圾回收(GC) 會比較好,一般來說主要的...
摘要:直接對棧的操作只有兩個,就是對棧幀的壓棧和出棧。中將永久代移除,同時增加元數據區。在中,本地方法棧和虛擬機棧是在同一塊兒區域,這完全取決于技術實現的決定,并未在規范中強制。 原文:https://github.com/linsheng97... 描述一下 JVM 的內存區域 程序計數?(PC,Program Counter Register)。在 JVM 規范中,每個線程都有它自己的...
面試官:今天要不來聊聊JVM調優相關的吧?面試官:你曾經在生產環境下有過調優JVM的經歷嗎?候選者:沒有面試官:...候選者:嗯...是這樣的,我們一般優化系統的思路是這樣的候選者:1. 一般來說關系型數據庫是先到瓶頸,首先排查是否為數據庫的問題候選者:(這個過程中就需要評估自己建的索引是否合理、是否需要引入分布式緩存、是否需要分庫分表等等)候選者:2. 然后,我們會考慮是否需要擴容(橫向和縱向都...
摘要:垃圾回收算法與垃圾回收器綜述我們常說的垃圾回收算法可以分為兩部分對象的查找算法與真正的回收方法。串行垃圾回收器一次只使用一個線程進行垃圾回收并行垃圾回收器一次將開啟多個線程同時進行垃圾回收。 垃圾回收算法與 JVM 垃圾回收器綜述歸納于筆者的 JVM 內部原理與性能調優系列文章,文中涉及的引用資料參考 Java 學習與實踐資料索引、JVM 資料索引。 showImg(https://s...
閱讀 1378·2021-09-26 09:55
閱讀 1917·2019-08-30 12:45
閱讀 1055·2019-08-29 11:20
閱讀 3554·2019-08-26 11:33
閱讀 3411·2019-08-26 10:55
閱讀 1685·2019-08-23 17:54
閱讀 2381·2019-08-23 15:55
閱讀 2341·2019-08-23 14:23