JVM 原理與優化 (1)

Do_your_best發表於2019-01-25

1、體系結構及生命週期

JVM 原理與優化 (1)

如圖所示,JVM包括類裝載器子系統、執行時資料區、執行引擎。類裝載器子系統根據給定的許可權 的名來裝入型別(類或者介面)。執行引擎負責執行那些包含在被裝載類的方法中的指令。執行時 資料區包含方法區、堆、Java棧、PC暫存器、本地方法棧。

類裝載器子系統:在JVM中負責查詢並裝載型別的那部分被稱為類裝載器子系統。JVM中有兩種類 裝載器:啟動類裝載器和使用者自定義類裝載器。類裝載器必須嚴格按照如下順序進行工作:

1)裝載:查詢並裝載型別的二進位制資料。

2)連線:執行驗證,準備,以及解析。

驗證——確保被匯入型別的正確性。

準備——為類變數分配記憶體,並將其初始化為預設值。

解析——把型別中的符號轉換為直接引用。

3)初始化:把型別變數初始化為正確的初始值

方法區:在Java虛擬機器中,關於被裝載型別的資訊儲存在一個邏輯上被稱為方法區的記憶體中,類中的類(靜態)變數同樣儲存在方法區中。所有執行緒共享方法區,因此,它們對方法區的資料訪問必須被設計為執行緒安全的。方法區的大小不必是固定的,虛擬機器可以根據應用的需要動態調整,同樣方法區也不必是連續的,也可以被垃圾收集。

JVM會在方法區中儲存以下型別資訊:

這個型別的全限定名; 這個型別的直接超類的全限定名(除非這個型別是java.lang.Object,沒有超類); 這個型別是類型別還是介面型別; 這個型別的訪問修飾符(public 、abstract或者final的某個子集); 任何直接超類的全限定名的有序列表。

除了上面列出的基本型別資訊外,JVM還得為每個被裝載的型別儲存以下資訊:

該類的常量池(常量池就是該型別所用常量的一個有序集合,包括直接常量和對其他型別、欄位、方法的符號引用); 欄位資訊; 除了常量以外的所有類(靜態)變數; 一個到Class類的引用。

:一個Java虛擬機器例項中只存在一個堆空間,因此所有執行緒都將共享這個堆。又由於一個Java程式獨佔一個JVM例項,因而每個Java程式都有它自己的堆空間——它們不會彼此干擾。但是,同一個Java程式的多個執行緒共享同一個堆空間。

程式計數器:每個執行緒都有自己的PC(程式計數器)暫存器,它執行緒啟動時建立,大小是一個字長。當執行本地方法時,PC暫存器的內容是下一條將被執行指令的地址;當執行本地方法時,PC暫存器中的值是“undefined”。

Java棧:每當啟動一個新的執行緒時,JVM都會為它分配一個Java棧。Java棧以幀為單位儲存執行緒的執行狀態,JVM只會直接對棧執行兩種操作:以幀為單位的壓棧或出棧。每當呼叫一個Java方法時,虛擬機器都會在該執行緒的Java棧中壓入一個新幀,使用這個幀來儲存引數、區域性變數、中間運算結果等資料。Java棧上的所有資料都是此執行緒所獨有的。

本地方法棧:本地方法本質上是依賴於實現的,JVM實現的設計者們可以自己決定使用怎樣的方式來讓Java程式呼叫本地方法。

執行引擎:在Java虛擬機器規範中,執行引擎的行為使用指令集來定義,具體內容及實現有待研究。

JVM生命週期

當啟動一個Java程式時,一個虛擬機器例項就誕生了,當程式關閉退出時,這個虛擬機器例項隨之消亡。JVM例項通過main()方法來執行一個Java
程式。而這個main()方法必須是共有的(public)、靜態的(static)、返回void,並且接收一個字串陣列為引數。Java程式初始類中的
main()方法,將作為改程式初始執行緒的起點,任何其他執行緒都是由這個初試執行緒啟動的。JVM內部有兩種執行緒:守護執行緒與非守護執行緒。守護
執行緒通常是由虛擬機器自己使用的,比如垃圾回收執行緒。當該程式所有的非守護執行緒都終止時,JVM例項將自動退出。
複製程式碼

2、JVM記憶體管理

1)程式計數器,也指pc暫存器

幾乎不佔有記憶體。用於取下一條執行的指令。

2)堆 所有通過new建立的物件的記憶體都在堆中分配,其大小可以通過-Xmx和-Xms來控制。堆被劃分為新生代和舊生代,新生代又被進一步劃分 為Eden和Survivor區,最後Survivor由FromSpace和ToSpace組成,(也指s0,s1)結構圖如下所示:

JVM 原理與優化 (1)

新生代。新建的物件都是用新生代分配記憶體,Eden空間不足的時候,會把存活的物件轉移到Survivor中,新生代大小可以由-Xmn來控制, 也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例。舊生代用於存放新生代中經過多次垃圾回收仍然存活的物件。

3)棧

每個執行緒執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括區域性變數區和運算元棧,用於存放此次方法呼叫過程中的臨時變數、數和中間結果。

4)本地方法棧

用於支援native方法的執行,儲存了每個native方法呼叫的狀態

5)方法區

存放了要載入的類資訊、靜態變數、final型別的常量、屬性和方法資訊。JVM用永久代(PermanetGeneration)來存放方法區, (在JDK的HotSpot虛擬機器中,可以認為方法區就是永久代,但是在其他型別的虛擬機器中,沒有永久代的概念)可通過-XX:PermSize 和-XX:MaxPermSize來指定最小值和最大值。

6)Java記憶體洩露和記憶體溢位

記憶體洩漏:分配出去的記憶體回收不了

記憶體溢位:指系統記憶體不夠用了

7)Java類載入機制

JVM將類載入過程劃分為三個步驟:裝載、連結和初始化。

裝載(Load):裝載過程負責找到二進位制位元組碼並載入至JVM中,JVM通過類的全限定名(com.bluedavy.HelloWorld)及類載入器 (ClassLoaderA例項)完成類的載入;

連結(Link):連結過程負責對二進位制位元組碼的格式進行校驗、初始化裝載類中的靜態變數及解析類中呼叫的介面、類;

初始化(Initialize):執行類中的靜態初始化程式碼、構造器程式碼及靜態屬性的初始化。

3、GC詳解

1) 垃圾收集的意義

在C++中,物件所佔的記憶體在程式結束執行之前一直被佔用,在明確釋放之前不能分配給其它物件;而在Java中,當沒有物件引用指向原先分配給某個物件的記憶體時,該記憶體便成為垃圾。JVM的一個系統級執行緒會自動釋放該記憶體塊。垃圾收集意味著程式不再需要的物件是"無用資訊",這些資訊將被丟棄。當一個物件不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新物件使用。事實上,除了釋放沒用的物件,垃圾收集也可以清除記憶體記錄碎片。由於建立物件和垃圾收集器釋放丟棄物件所佔的記憶體空間,記憶體會出現碎片。碎片是分配給物件的記憶體塊之間的空閒記憶體洞。碎片整理將所佔用的堆記憶體移到堆的一端,JVM將整理出的記憶體分配給新的物件。

  垃圾收集能自動釋放記憶體空間,減輕程式設計的負擔。這使Java 虛擬機器具有一些優點。首先,它能使程式設計效率提高。在沒有垃圾收集機制的時候,可能要花許多時間來解決一個難懂的儲存器問題。在用Java語言程式設計的時候,靠垃圾收集機制可大大縮短時間。其次是它保護程式的完整性, 垃圾收集是Java語言安全性策略的一個重要部份。

  垃圾收集的一個潛在的缺點是它的開銷影響程式效能。Java虛擬機器必須追蹤執行程式中有用的物件,而且最終釋放沒用的物件。這一個過程需要花費處理器的時間。其次垃圾收集演算法的不完備性,早先採用的某些垃圾收集演算法就不能保證100%收集到所有的廢棄記憶體。當然隨著垃圾收集演算法的不斷改進以及軟硬體執行效率的不斷提升,這些問題都可以迎刃而解。

  一般來說,Java開發人員可以不重視JVM中堆記憶體的分配和垃圾處理收集,但是,充分理解Java的這一特性可以讓我們更有效地利用資源。同時要注意finalize()方法是Java的預設機制,有時為確保物件資源的明確釋放,可以編寫自己的finalize方法。

2) 垃圾收集演算法

 2.1 標記-清除演算法(Mark-Sweep):
複製程式碼

標記-清除演算法分為標記和清除兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,標記過程其實就是根搜尋演算法判斷物件是否存活。該演算法主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大的物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。標記-清除演算法的執行過程如下圖所示:

JVM 原理與優化 (1)

2.2 複製演算法(Coping):
複製程式碼

複製演算法是把記憶體分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的物件複製到另一塊上,然後把這塊記憶體整個清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時就不用考慮記憶體碎片等複雜情況,實現簡單,執行高效。這種方法適用於短生存期的物件,持續複製長生存期的物件則導致效率降低。複製演算法的執行過程如下圖所示:

JVM 原理與優化 (1)

2.3 標記-整理演算法(Mark-Compact):
複製程式碼

複製演算法在物件存活率較高時就要進行較多的複製操作,效率將會降低。老年代更常見的情況是大部分物件都是存活物件。如果依然使用複製演算法,由於存活的物件較多,複製的成本也將很高。標記-整理演算法是一種老年代的回收演算法,該演算法與標記-清除演算法的標記過程一樣,但是之後不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,其價效比比較高。該演算法示意圖如下圖所示:

JVM 原理與優化 (1)

2.4分代收集演算法:
複製程式碼

根據垃圾回收物件的特性,不同階段最優的方式是使用合適的演算法用於本階段的垃圾回收,分代演算法即是基於這種思想,它將記憶體區間根據物件的特點分成幾塊,根據每塊記憶體區間的特點,使用不同的回收演算法,以提高垃圾回收的效率。一般把java堆分為新生代和老年代,新生代採用複製演算法,老年代採用標記-整理演算法。

JVM 原理與優化 (1)

新生代(Young Generation):用於存放新建立的物件,採用複製回收方法,如果在s0和s1之間複製一定次數後,轉移到年老代中。這裡的垃圾回收叫做minor GC;

年老代(Old Generation):這些物件垃圾回收的頻率較低,採用的標記整理方法,這裡的垃圾回收叫做 major GC。

永久代(Permanent Generation):存放Java本身的一些資料,當類不再使用時,也會被回收。

這裡可以詳細的說一下新生代複製回收的演算法流程:

在新生代中,分為三個區:Eden, from survivor, to survior。

當觸發minor GC時,會先把Eden中存活的物件複製到to Survivor中;

然後再看from survivor,如果次數達到年老代的標準,就複製到年老代中;如果沒有達到則複製到to survivor中,如果to survivor滿了,則複製到年老代中。

然後調換from survivor 和 to survivor的名字,保證每次to survivor都是空的等待物件複製到那裡的
複製程式碼

3) 常見的垃圾收集器

    下面一張圖是HotSpot虛擬機器包含的所有收集器
複製程式碼

JVM 原理與優化 (1)

3.1 Serial收集器:
複製程式碼

這個收集器是一個單執行緒收集器,使用複製收集演算法,收集時會暫停所有工作執行緒,直到收集結束,虛擬機器執行在Client模式時的預設新生代收集器。優點是:簡單高效(與其他收集器的單執行緒相比),對於限定單個CPU的環境來說,Serial收集器沒有現成互動的開銷,做垃圾收集可以獲得最高的單執行緒收集效率。如下圖:

JVM 原理與優化 (1)

 3.2 ParNew收集器:
複製程式碼

ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為包括演算法、STW、物件分配規則、回收策略等都與Serial收集器一樣。ParNew收集器是許多執行在server模式下的虛擬機器中首選的新生代收集器,一個重要原因是在除了serial收集器外,目前只有它能與CMS收集器配合使用。ParNew收集器在單CPU環境中不比Serial效果好,甚至可能更差,兩個CPU也不一定跑的過,但隨著CPU數量的增加,效能會逐步增加。ParNew收集器的工作過程如下:

JVM 原理與優化 (1)

 3.3 Parallel Scavenge 收集器:
複製程式碼

ParallelScavenge 收集器是一個新生代收集器,它是使用複製演算法的並行多執行緒的收集器。

ParallelScavenge的特點是它的關注點與其他收集器不同,CMS等收集器的關注點儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用於執行使用者程式碼時間與CPU總消耗時間的比值。吞吐量=執行使用者程式碼時間/執行使用者程式碼時間+垃圾收集時間。

高吞吐量和停頓時間短的策略相比,主要強調高效率地利用CPU時間,任務更快完成,適用於後臺運算而不需要太多互動的任務;而後者強呼叫戶互動體驗。

 3.4  Serial Old收集器:
複製程式碼

單執行緒收集器,是Serial收集器老年代版本,使用“標記-整理”演算法,主要用在client模式下,如果在Server模式下,它主要有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge 收集器搭配使用;另一用途是作為CMS收集器的後備預案,在併發手機發生CMF時使用。

3.5 Parallel Old 收集器:
複製程式碼

Parallel Old是ParallelScavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。Parallel Old收集器的工作過程如下圖:

JVM 原理與優化 (1)

3.6  CMS收集器:
複製程式碼

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

JVM 原理與優化 (1)

(1)初始標記(CMS initial mark):標記GC Roots能直接關聯到的物件,速度很快。

(2)併發標記(CMS concurrent mark):進行GC ROOTS 根搜尋演算法階段,會判定物件是否存活。

(3)重新標記(CMS remark):修正併發標記期間因使用者程式繼續執行而導致標記發生改變的那一部分物件的標記記錄。

(4)併發清除(CMS concurrent sweep)

其中初始標記和重新標記兩個階段仍然需要Stop-The-World,整個過程中耗時最長的併發標記和併發清除過程中收集器都可以和使用者執行緒一起工作。所以整體來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

CMS收集器的優點:併發收集、低停頓,但是CMS還遠遠達不到完美,器主要有三個顯著缺點:

(1)CMS收集器對CPU資源非常敏感。在併發階段,雖然不會導致使用者執行緒停頓,但是會佔用CPU資源而導致引用程式變慢,總吞吐量下降。CMS預設啟動的回收執行緒數是:(CPU數量+3) / 4。

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

(3)最後一個缺點,CMS是基於“標記-清除”演算法實現的收集器,使用“標記-清除”演算法收集後,會產生大量碎片。空間碎片太多時,將會給物件分配帶來很多麻煩,比如說大物件,記憶體空間找不到連續的空間來分配不得不提前觸發一次Full GC。

3.7 G1收集器:
複製程式碼

Parallel Old是ParallelScavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。Parallel Old收集器的工作過程如下圖:

JVM 原理與優化 (1)

G1收集器是一款面向服務端應用的垃圾收集器,用於替換CMS收集器。與其他GC收集器相比,G1具有以下幾個特點:

(1)並行與併發:充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓時間,在收集過程中用併發的方式讓Java執行緒繼續執行。

(2)分代收集:仍然有分代的概念,不需要其他收集器配合能獨立管理整個GC堆,能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的就物件以獲得更好的收集效果。

(3)空間整合:G1從整體看,是基於“標記-整理”演算法實現的,從區域性(兩個Region之間)看是基於“複製”演算法的。在執行期間不會產生記憶體碎片,有利於程式長時間執行分配大物件時不會因為無法找到連續記憶體而提前出發下一次GC。

(4)可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型

G1收集器運作大致可以分為以下幾個步驟:

(1)初始標記:只標記GC Roots能直接關聯到的物件,並且修改TAMS(Next Topat Mark Start)值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件。此階段需要停頓使用者執行緒。

(2)併發標記:從GC Roots開始對堆中物件進行可達性分析,找出存活物件;耗時較長,可與使用者執行緒併發執行。

(3)最終標記:修正在併發標記期間有變動的標記記錄,這階段需要停頓執行緒,可以並行執行。

(4)篩選回收:對各個Region的回收價值和成本進行排序,根據使用者期望的GC停頓時間制定回收計劃,進行垃圾回收。

相關文章