對《深入理解Java虛擬機》的總結(一)

Java Java虛擬機 編程語言 CPU Java團長 2017-05-19

這是《深入理解Java虛擬機》第二章和第三章的讀書筆記。

Java內存區域

以下的這張圖給出了JVM所管理的內存在運行時的數據區域:

對《深入理解Java虛擬機》的總結(一)

JVM棧:它的生命週期和線程相同。它描述的是Java方法執行的內存模型:每個方法被執行的時候都會創建一個棧幀用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。

Java堆:Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的是存放對象的實例,幾乎所有的對象實例都在這裡進行分配內存。是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。可分為新生代和老年代。

方法區:和Java堆一樣是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。也稱為永久代。

運行時常量池:是方法區的一部分。用於存放編譯期生成的各種字面量和符號引用。它具備動態性。

程序計數器:它的作用可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變程序計數器的值來選取下一條需要執行的字節碼指令(分支、循環、跳轉、異常處理等)。在任何一個確定的時刻,一個處理器只會執行一條線程中的指令。

本地方法棧:與JVM棧相似。本地方法棧服務於虛擬機執行Native方法,JVM棧服務於執行Java方法。

對象訪問

在最簡單的訪問中也會涉及Java棧、Java堆、方法區這三個最重要的內存區域之間的關係。

比如在代碼:Object obj = new Object();如果該語句出現在方法體中,那麼Object obj這一部分的語義將會反映在本地變量表中,作為一個reference類型數據出現。new Object()這部分的語義將反映到Java堆中,形成一塊存儲了Object類型所有實例數據值的結構化內存,根據具體類型以及虛擬機實現的對象內存佈局的不同,這塊內存的長度是不固定的。另外在方法區中還存儲有能找到次對象類型數據的地址信息。

主流訪問對象的方式有兩種:使用句柄和直接指針。

  • 使用句柄的方式:Java堆中將會劃出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。如下圖:

對《深入理解Java虛擬機》的總結(一)

  • 直接指針的方式:Java堆對象的佈局中必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象地址。如下圖:

對《深入理解Java虛擬機》的總結(一)

使用句柄訪問方式的最大的好處就是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中實例數據指針,而reference本身不需要被修改。使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多也是一項非常可觀的執行成本。

判斷對象是否還活著的算法

引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數值就減1;任何時刻計數器都未0的對象就是不可能再被使用的。

ps:JVM不是通過使用引用計數算法來判斷對象是否存活的。

根搜索算法(GC Roots Tracing)

Java虛擬機使用該算法判斷對象是否存活的。

基本思路:通過一系列的名為“GC Roots”的對象作為起始點,從這些起始點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈想連(用圖論的話來說就是從GC Roots到這個對象不可達)時,則證明此對象是不可用對象。

在Java語言中,可以作為GC Roots的對象包括:

  • 虛擬機棧(棧幀中的本地變量表)中的引用的對象。

  • 方法區中的類靜態屬性引用的對象。

  • 方法區中的常量引用的對象。

  • 本地方法棧中JNI的引用的對象。

4種引用類型

引用可以分為強引用(Strong Reference)軟引用(Soft Reference)弱引用(Weak Reference)虛引用(Phantom Reference)

  • 強引用就是指在程序代碼中普遍存在的,類似Object obj = new Object()這類的引用。只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象。

  • 軟引用用老描述一些還有用的,但並非必須的對象。對於軟引用關聯著的對象,在系統將要發生內存異常之前,將會把這些對象列進回收範圍之中並進行第二次回收。如果這次回收還是沒有足夠的內存,才會拋出內存溢出異常。JDK提供SoftReference類來實現軟引用。

  • 弱引用也是用來描述非必須對象的,它比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。JDK提供WealReference類來實現弱引用。

  • 虛引用是最弱的引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用的唯一目的是希望能在這個對象被收集器回收的時候收到一個系統通知。JDK中使用PhantomReference類實現。

判斷一個對象是生存還是死亡的算法

在根搜索算法中不可達的對象,並非是必須死亡的。要真正宣告一個對象的死亡,至少需要經歷兩次標記過程:如果對象在進行根搜索後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法以及被虛擬機調用過,虛擬機將這兩種情況視為“沒有必要執行”,那麼這個對象就可以死亡了。如果這個對象被判斷為有必要執行finalize()方法,那麼這個對象將會被放置在一個叫做F-Queue的隊列中,並在稍後由一條由虛擬機自動建立的、低優先級的Finalize線程去執行(虛擬機執行這個方法,但是不保證運行結束)。finalize()方法是對象逃離死亡的最後一次機會,GC將對F-Queue的對象進行第二次標記,如果對象在finalize()中重新建立了引用,那麼就不會死亡,否則將會死亡。

垃圾收集算法

以下介紹了“標記-清除算法”、“複製算法”、“標記-整理算法”以及“分代收集算法”。

標記-清除算法

標記-清除算法(Mark-Sweep)是最基本的算法,分為“標記”和“清除”兩個階段。首先標記處所有需要回收的對象,在標記完成之後就統一清除掉所有被標記的對象。

主要缺點:

  • 效率問題。標記和清除的過程效率都不高

  • 空間問題。標記清除以後會產生大量不連續的空間碎片,空間碎片太多會導致程序以後的內存分配問題。

對《深入理解Java虛擬機》的總結(一)

複製算法

複製算法為了解決效率問題。它將可同內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當一塊的內存用完了,就將還存活的對象複製到另一塊上,然後再把原先那塊內存空間一次清理掉。這樣,就每次只對一塊內存進行分配,也不用考慮內存碎片問題。

主要缺點:

  • 沒存縮小為原來的一半,代價高。

  • 在對象存活率較高的時候就要執行較多的複製操作,效率將會變低。

對《深入理解Java虛擬機》的總結(一)

現在的商業虛擬機都採用這種算法回收新生代。

在新生代中,有一塊比較大的Eden和兩塊比較小的Survivor空間,每次使用Eden和其中的一塊Survivor;回收的時候,將Eden和Survivor中還活著的對象一次性拷貝到另一塊Survivor上,清除已被使用的Eden和Survivor。當Survivor不夠用的時候,使用老年代進行分擔。

所以,默認的Eden和兩塊Survivor大小比為8:1:1。

標記-整理算法

Mark-Compact算法的標記過程和“標記-清除算法”一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉邊界以外的內存。

對《深入理解Java虛擬機》的總結(一)

分代收集算法

當前的商業虛擬機都採用“分代收集(Generation Collection)”算法。這種算法根據對象的存活週期的不同將內存劃分為幾塊。一般把Java的堆分為新生代老年代,這樣就可以根據各個年代的特點採用最適當地收集算法。

在新生代中,每次垃圾收集時都有大量對象死去,只有少量存活,那就使用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。

在老年代中,對象存活率高,沒有額外空間對它進行分配擔保,使用“標記-清除”算法或者“標記-整理”算法

垃圾回收器

收集算法是內存回收的方法論,垃圾收集器就是內存回收的具體實現。

對《深入理解Java虛擬機》的總結(一)

Serial收集器

這個收集器是個單線程收集器。它在工作的時候必須暫停其他所有的工作線程(Stop The World),直到它收集結束。這項工作實際上是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把用戶的正常工作的線程停止掉,然後進行垃圾收集。

它是虛擬機運行在Client模式下的默認新生代收集器。優於其他收集器的地方是:簡單而高效。

對《深入理解Java虛擬機》的總結(一)

ParNew收集器

ParNew收集器是Serial收集器的多線程版本。在控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器一樣。

它是虛擬機運行在Server模式下的默認新生代收集器。它能與CMS收集器配合工作。它默認開啟的線程數和CPU的數量相同。

對《深入理解Java虛擬機》的總結(一)

Parallel Scavenge收集器

它也是一個新生代收集器,也是使用複製算法,是並行的多線程收集器。它的目標是達到一個可控制的吞吐量(Throughput = 運行用戶代碼的時間/(運行用戶代碼的時間+垃圾回收時間))。所以被稱為“吞吐量優先”收集器。

主要適用於在後臺運行而不需要太多交互的任務。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本。也是一個單線程收集器,使用“標記-整理”算法。

它主要是被在Client模式下虛擬機使用。如果在Server模式下,它有:1)在1.5及以前版本中與Parallel Scavenge收集器搭配使用;2)作為CMS收集器的後備預案,在收集器發生Concurrent Mode Failure的時候使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。

對《深入理解Java虛擬機》的總結(一)

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它是基於“標記-清除”算法實現的。整個過程分為4個步驟:

  • 初始標記(CMS initial mark)

  • 併發標記(CMS concurrent mark)

  • 重新標記(CMS remark)

  • 併發清除(CMS concurrent sweep)

初始標記需要進行Stop The World,它僅僅是標記一下GC Roots能直接關聯到的對象,速度很快;

併發標記就是進行GC Roots Tracing的過程;這個階段的耗時比較長;

重新標記也需要進行Stop The World,該階段是為了修正併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。這個階段的停頓時間會比初始標記長些而遠遠短於併發標記;

併發清除就是進行清除的過程;這個階段的耗時比較長;

CMS收集器的內存回收過程是與用戶線程一起併發地執行的。

CMS收集器很符合現在互聯網或者B/S系統服務器的需求——重視服務的響應速度、希望系統停頓時間短。

CMS收集器的顯著缺點:

  • CMS收集器對CPU資源非常敏感。CMS默認的回收線程數為:(CPU數量+3)/4。也就是當CPU在4個以上的時候,併發回收時垃圾收集器線程最多佔用不超過25%的CPU資源;當CPU不足4個的時候,那麼CMS對用戶程序的影響就比較大。

  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能會出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。

  • CMS收集器採用“標記-清除”算法,在收集結束的時候可能會產生大量的空間碎片。

對《深入理解Java虛擬機》的總結(一)

G1收集器

Garbage First收集器。基於“標記-整理”算法,可以非常顯著的控制停頓。

特點:

  • 並行與併發:和CMS類似。

  • 分代收集:保留了新生代和來年代的概念,但新生代和老年代不再是物理隔離的了它們都是一部分Region(不需要連續)的集合。同時,為了避免全堆掃描,G1使用了Remembered Set來管理相關的對象引用信息。

  • 空間整合:由於G1使用了獨立區域(Region)概念,G1從整體來看是基於“標記-整理”算法實現收集,從局部(兩個Region)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片。

  • 可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明確指定一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

步驟:

  • 初始標記(Initial Making)

  • 併發標記(Concurrent Marking)

  • 最終標記(Final Marking)

  • 篩選回收(Live Data Counting and Evacuation)

初始階段僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS(Next Top Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可以用的Region中創建新對象,這個階段需要停頓線程,但耗時很短。併發標記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這一階段耗時較長但能與用戶線程併發運行。而最終標記階段需要吧Remembered Set Logs的數據合併到Remembered Set中,這階段需要停頓線程,但可並行執行。最後篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃,這一過程同樣是需要停頓線程的,但Sun公司透露這個階段其實也可以做到併發,但考慮到停頓線程將大幅度提高收集效率,所以選擇停頓。

對《深入理解Java虛擬機》的總結(一)

內存配置與回收策略

Java技術體系中的自動內存管理最終可以歸納為自動化地解決了兩個問題:給對象分配內存以及回收分配給對象的內存。

對《深入理解Java虛擬機》的總結(一)

對象優先在Eden中分配

大多數情況下,對象在新生代Eden區中分配。檔Eden區沒有足夠的空間進行分配時,虛擬機將發起一次Minor GC。

-Minor GC:新生代GC,指發生在新生代的垃圾收集動作,因為Java對象大多數都具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也非常快。

-Major GC/Full GC:老年代GC,指發生在老年代的GC,出現了Major GC經常就會至少有一次Minor GC。Major GC的速度比Minor GC慢10倍以上。

大對象直接進入老年代

大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

長期存活的對象將進入老年代

虛擬機給每個對象定義一個年齡計數器,對象在Eden中經歷第一次Minor GC仍然存活,就被移動到Survivor空間,設置年齡為1,在Survivor空間中沒經歷一次Minor GC,年齡加1,當達到默認的年齡15以後,就將被放到老年代。

動態對象年齡判定

如果在Survivor空間中,相同年齡所有對象大小的總和大於Survivor空間的一般,那麼年齡大於或等於該年齡的對象就可以直接進入老年代。

空間分配擔保

發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於,則改為直接進行一次Full GC。如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗;如果允許,則只會進行Minor GC;如果不允許,則也要改為進行一次Full GC。

新生代Eden,Survivor A, Survivor B三塊空間和老生代Old之間的流程關係:

對《深入理解Java虛擬機》的總結(一)

學習Java的同學注意了!!!

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群495273252,我們一起學Java!

相關推薦

推薦中...