解讀JVM虛擬機
- 2020 年 10 月 15 日
- 筆記
概要點:
- java虛擬機概述和基本概念
- 堆、棧、方法區
- 了解虛擬機參數
- 垃圾回收概念和演算法、及對象的分代轉換
- 垃圾收集器
java虛擬機的原理:
- 所謂虛擬機,就是一台虛擬的機器。它是一款軟體,用來執行一系列虛擬電腦指令,大體上虛擬機可以分為系統虛擬機和程式虛擬機,大名鼎鼎的Visual Box、VMare就屬於系統虛擬機,他們完全是對物理電腦的模擬,提供了一個可運行完整作業系統的軟體平台。
- 程式虛擬機典型代表就是Java虛擬機,它專門為執行單個電腦程式而設計,在java虛擬機中執行的指令我們成為java位元組碼指令。無論是系統虛擬機還是程式虛擬機,在上面運行的軟體都被限制於虛擬機提供的資源中。Java發展至今,出現過很多虛擬機,最初Sun使用的一款叫Classic的Java虛擬機,到現在引用最廣泛的是HotSpot虛擬機,除了Sun意外,還有BEA的JRockit,目前JRockit和HotSpot都被Oracle收入旗下,大有整合的趨勢。
java虛擬機的基本結構
結構概念說明:
- 類載入子系統:負責從文件系統或者網路中載入Class資訊,載入的資訊 存放在一塊稱之為方法區的記憶體空間。
- 方法區:就是存放類資訊、常量資訊、常量池資訊、包括字元串字面量和數字常量等。
- java堆:在java虛擬機啟動的時候建立java堆,它是java程式最主要的記憶體工作區域,幾乎所有的對象實例都存放到java堆中,堆空間是所有執行緒共享的。
- 直接記憶體:Java的NIO庫允許java程式使用直接記憶體,從而提高性能,通常直接記憶體速度會優於java堆。讀寫頻繁的場合可能會考慮使用。
- 每個虛擬機執行緒都有一個私有的棧,一個執行緒的java棧在執行緒創建的時候被創建,java棧中保存著局部變數、方法參數、同時java的方法調用、返回值等。
- 本地方法棧和java棧非常類似,最大不同為本地方法棧用於本地方法調用。java虛擬機允許java直接調用本地方法(通常使用C編寫)。
- 垃圾收集系統是java的核心,也是必不可少的,java有一套自己進行垃圾清理的機制,開發人員無需手工清理,我們稍後詳細說明。
- PC(Program Counter)暫存器也是每個執行緒私有的空間,java虛擬機會為每個執行緒創建PC暫存器,在任意時刻,一個java執行緒總是在執行一個方法,這個方法被稱為當前方法,如果當前方法不是本地方法,PC暫存器就會執行當前正在被執行的指令,如果是本地方法,則PC暫存器值為undefined,暫存器存放如當前執行環境指針、程式計數器、操作棧指針、計算的變數指針等資訊。
-
虛擬機最核心的組件就是執行引擎了,它負責執行虛擬機的位元組碼。一般戶先進行編譯成機器碼後執行
堆、棧、方法區概念和聯繫:
- 堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。 棧解決程式的運行問題,即程式如何執行,或者說如何處理數據。
- 方法區則是輔助堆棧的快永久區(Perm),解決堆棧資訊的產生,是先決條件。
- 我們創建一個新的對象,User:那麼User類的一些資訊(類資訊、靜態資訊都存在於方法區中) 而User類被實例化出來之後,被存儲到java堆中,一塊記憶體空間 當我們去使用的時候,都是使用User對象的引用,形如User user = new User(); 這裡的user就是存放在java棧中的,即User真實對象的一個引用。
java棧:
- java棧是一塊執行緒私有的記憶體空間,一個棧,一般由三部分組成:局部變數表、操作數棧和幀數據區。
- 局部變數表:用於報錯函數的參數及局部變數。 操作數棧:主要保存計算過程的中間結果,同時作為計算過程中變數臨時的存儲空間。
- 幀數據區:除了局部變數表和操作數棧以外,棧還需要一些數據來支援常量池的解析,這裡幀數據區保存著訪問常量池的指針,方便程式訪問常量池。
- 另外,當函數返回或者出現異常時,虛擬機必須有一個異常處理表,方便發送異常的時候找到異常的程式碼,因此異常處理表也是幀數據區的一部分。
java方法區:
- java方法區和堆一樣,方法區是一塊所有執行緒共享的記憶體區域,它保存系統的類資訊,比如類的欄位、方法、常量池等。
- 方法區的大小決定了系統可以保存多少個類,如果系統定義太多的類,導致方法區溢出。虛擬機同樣會拋出記憶體溢出錯誤。方法區可以理解為永久區(Perm)
虛擬機參數:
- 在虛擬機運行的過程中,如果可以跟蹤系統的運行狀態,那麼對於問題的故障排查會有一定的幫助。
- 為此,虛擬機提供了一些跟蹤系統狀態的參數,使用給定的參數執行java虛擬機,就可以在系統運行時列印相關日誌,用於分析實際問題。我們進行虛擬機參數配置,其實主要就是圍繞著堆、棧、方法區進行配置。
堆分配參數(一):
- -XX:+PrintGC 使用這個參數,虛擬機啟動後,只要遇到GC就會列印日誌。
- -XX:+UseSerialGC 配置串列回收器
- -XX:+PrintGCDetails 可以查看詳細資訊,包括各個區的情況
- -Xms:設置java程式啟動時初始堆大小
- -Xmx:設置java程式能獲得的最大堆大小
- -Xmx20m -Xms5m -XX:+PrintCommandLineFlags : 可以將隱式或者顯示傳給虛擬機的參數輸出
- 總結:在實際工作中,我們可以直接將初始的堆大小與最大堆大小設置相等,這樣的好處是可以減少程式運行時的垃圾回收次數,從而提高性能。
堆分配參數(二):
- 新生代的配置 -Xmn:可以設置新生代的大小,設置一個比較大的新生代會減少老年代的大小,這個參數對系統性能以及GC行為有很大的影響,新生代大小一般會設置整個堆空間的1/3到1/4左右。
- -XX:SurvivorRatio:用來設置新生代中eden空間和from/to空間的比例。含義:-XX:SurvivorRatio=eden/from=eden/to
-
總結:不同的堆分布情況,對系統執行會產生一定的影響,在實際工作中,應該根據系統的特點做出合理的配置,基本策略:儘可能將對象預留在新生代,減少老年代的GC次數。 除了可以設置新生代的絕對大小(-Xmn),還可以使用(-XX:NewRatio)設置新生代和老年代的比例:-XX:NewRatio=老年代/新生代
堆溢出處理:
- 在java程式的運行過程中,如果堆空間不足,則會拋出記憶體溢出的錯誤(Out Of Menory)OOM,一旦這類問題發生在生產環境,可能引起嚴重的業務中斷。
- java虛擬機提供了-XX:+HeapDumpOnOutOfMemoryError,使用該參數可以在記憶體溢出時導出整個堆資訊,與之配合使用的還有參數, -XX:HeapDumpPath,可以設置導出堆的存放路徑。
棧配置:
- Java虛擬機提供了參數-Xss來指定執行緒的最大棧空間,整個參數也直接決定了函數可調用的最大深度。
方法區:
- 和java堆一樣,方法區是一塊所有執行緒共享的記憶體區域,它用於保存系統的類資訊,方法區(永久區)可以保存多少資訊可以對其進行配置。
- 在默認情況下,-XX:MaxPermSize為64MB,如果系統運行時生產大量的類,就需要設置一個相對合適的方法區,以免出現永久區記憶體溢出的問題。 -XX:PermSize=64M -XX:MaxPermSize=64M
直接記憶體配置:
- 直接記憶體也是java程式中非常重要的組成部分,特別是廣泛用在NIO中,直接記憶體跳過了java堆,使java程式可以直接訪問原生堆空間,因此在一定程度上加快了記憶體空間的訪問速度。但是說直接記憶體一定就可以提高記憶體訪問速度也不見得,具體情況具體分析。
- 相關配置參數:-XX:MaxDirectMemorySize,如果不設置默認值為最大堆空間,即-Xmx。直接記憶體使用達到上限時,就會觸發垃圾回收,如果不能有效的釋放空間,也會引起系統的OOM.
垃圾回收概念和其演算法:
- 談到垃圾回收(Garbage Collection,簡稱GC),需要先澄清什麼是垃圾,類比日常生活中的垃圾,我們會把他們丟入垃圾桶,然後倒掉。
- GC中的垃圾,特指存於記憶體中、不會再被使用的對象,而回收就是相當於把垃圾「倒掉」。 垃圾回收有很多種演算法:如引用計數法、標記壓縮法、複製演算法、分代、分區的思想。
垃圾收集演算法(一):
- 引用計數法:這是個比較古老而經典的垃圾收集演算法,其核心就是在對象被其他所引用時計數器加1,而當引用失效時則減1,但是這種方式有非常嚴重的問題:無法處理循環引用的情況、還有就是每次進行加減操作比較浪費系統性能。 標記清除法:就是分為標記和清除倆個階段進行處理記憶體中的對象,當然這種方式也有非常大的弊端,就是空間碎片問題,垃圾回收後的空間不是連續的,不連續的記憶體空間的工作效率要低於連續的記憶體空間。
- 複製演算法:其核心思想就是將記憶體空間分為兩塊,,每次只使用其中一塊,在垃圾回收時,將正在使用的記憶體中的存留對象複製到未被使用的記憶體塊中去,之後去清除之前正在使用的記憶體塊中所有的對象,反覆去交換倆個記憶體的角色,完成垃圾收集。(java中新生代的from和to空間就是使用這個演算法)
- 標記壓縮法:標記壓縮法在標記清除法基礎之上做了優化,把存活的對象壓縮到記憶體一端,而後進行垃圾清理。(java中老年代使用的就是標記壓縮法) 考慮一個問題:為什麼新生代和老年代使用不同的演算法?
垃圾收集演算法(二):
- 分代演算法:就是根據對象的特點把記憶體分成N塊,而後根據每個記憶體的特點使用不同的演算法。 對於新生代和老年代來說,新生代回收頻率很高,但是每次回收耗時都很短,而老年代回收頻率較低,但是耗時會相對較長,所以應該盡量減少老年代的GC.
- 分區演算法:其主要就是將整個記憶體分為N多個小的獨立空間,每個小空間都可以獨立使用,這樣細粒度的控制一次回收都少個小空間和那些個小空間,而不是對整個空間進行GC,從而提升性能,並減少GC的停頓時間。
垃圾回收時的停頓現象:
- 垃圾回收器的任務是識別和回收垃圾對象進行記憶體清理,為了讓垃圾回收器可以高效的執行,大部分情況下,會要求系統進入一個停頓的狀態。
- 停頓的目的是終止所有應用執行緒,只有這樣系統才不會有新的垃圾產生,同事停頓保證了系統狀態在某一個瞬間的一致性,也有益於更好低標記垃圾對象。因此在垃圾回收時,都會產生應用程式的停頓。
對象如何進入老年代:
- 一般而言對象首次創建會被放置在新生代的eden區,如果沒有GC介入,則對象不會離開eden區,那麼eden區的對象如何進入老年代呢?
- 一般來講,只要對象的年齡達到一定的大小,就會自動離開年輕代進入老年代,對象年齡是由對象經曆數次GC決定的,在新生代每次GC之後如果對象沒有被回收則年齡加1.虛擬機提供了一個參數來控制新生代對象的最大年齡,當超過這個年齡範圍就會晉陞老年代。 -XX:MaxTenuringThreshold,默認情況下為15。
-
總結:根據設置MaxTenuringThreshold參數,可以指定新生代對象經過多少次回收後進入老年代。 另外,大對象(新生代eden區無法裝入時,也會直接進入老年代)。JVM里有個參數可以設置對象的大小超過在指定的大小之後,直接晉陞老年代。 -XX:PretenureSizeThreshold
-
總結:使用PretenureSizeThreshold可以進行指定進入老年代的對象大小,但是要注意TLAB區域優先分配空間。
對象創建流程圖:
垃圾收集器:
- 在java虛擬機中,垃圾回收器不僅僅只有一種,什麼情況下該使用哪種,對性能又有什麼樣的影響,這都是我們需要了解的。
- 串列垃圾回收器
- 並行垃圾回收器
- CMS回收器
- G1回收器
串列回收器:
- 串列回收器是指使用單執行緒進行垃圾回收的回收器。每次回收時,串列回收器只有一個工作執行緒,對於並行能力較弱的電腦來說,串列回收器的專註性和獨佔性往往有更好的性能表現。
- 串列回收器可以在新生代和老年代使用,根據作用於不同的堆空間,分為新生代串列回收器和老年代串列回收器。 使用-XX:+UseSerialGC 參數可以設置使用新生代串列回收器和老年代串列回收器
並行回收器(ParNew回收器):
- 並行回收器在串列回收器基礎上做了改進,他可以使用多個執行緒同時進行垃圾回收,對於計算能力強的電腦而言,可以有效的縮短垃圾回收所需的實際時間。
- ParNew回收器是一個工作在新生代的垃圾收集器,他只是簡單的將串列回收器多執行緒化,他的回收策略和演算法和串列回收器一樣。
- 使用 -XX:+UseParNewGC 新生代ParNew回收器,老年代則使用串列回收器 ParNew回收器工作時的執行緒數量可以使用
- -XX:ParallelGCThreads參數指定,一般最好和電腦的CPU相當,避免過多的執行緒影響性能。
並行回收器(ParallelGC回收器):
- 新生代ParallelGC回收器,使用了複製演算法的收集器,也是多執行緒獨佔形式的收集器,但ParallelGC回收器有個非常重要的特點,就是它非常關注系統的吞吐量。
- 提供了倆個非常關鍵的參數控制系統的吞吐量
- -XX:MaxGCPauseMillis:設置最大垃圾收集停頓時間,可用把虛擬機在GC停頓的時間控制在MaxGCPauseMillis範圍內,如果希望減少GC停頓時間可以將MaxGCPauseMillis設置的很小,但是會導致GC頻繁,從而增加了GC的總時間,降低了吞吐量。所以需要根據實際情況設置該值。
- -XX:GCTimeRatio:設置吞吐量大小,它是一個0到100之間的整數,默認情況下他的取值是99,那麼系統將花費不超過1/(1+n)的時間用於垃圾回收,也就是1/(1+99) = 1%的時間。
- 另外還可以指定 -XX:+UseAdaptiveSizePolicy打開自適應模式,在這種模式下,新生代的大小、eden、from/to的比例,以及晉陞老年代的對象年齡參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。
並行回收器(ParallelOldGC回收器):
- 老年代ParallelOldGC回收器也是一種多執行緒的回收器,和新生代的ParallelGC回收器一樣,也是一種關注吞吐量的回收器,他使用了標記壓縮演算法進行實現。
- -XX:+UseParallelOldGC 進行設置
- -XX:+ParallelGCThreads 也可以設置垃圾收集時的執行緒數量。
CMS回收器:
- CMS全稱為:Concurrent Mark Sweep 意為並發標記清除,他使用的是標記清除法,主要關注系統停頓時間。
- 使用 -XX:+UseConcMarkSweepGC 進行設置。
- 使用 -XX:ConcGCThreads 設置並發執行緒數量。
- CMS並不是獨佔的回收器,也就說CMS回收的過程中,應用程式仍然在不停的工作,又會有新的垃圾不斷的產生,所以在使用CMS的過程中應該確保應用程式的記憶體足夠可用。CMS不會等到應用程式飽和的時候才去回收垃圾,而是在某一閥值的時候開始回收,回收閥值可用指定的參數進行配置,-XX:CMSInitiatingOccupancyFraction來指定,默認為68,也就是說當老年代的空間使用率達到68%的時候,會執行CMS回收。如果記憶體使用率增長的很快,在CMS執行的過程中,已經出現了記憶體不足的情況,此時CMS回收就會失敗,虛擬機將啟動老年代串列回收器進行垃圾回收,這會導致應用程式中斷,知道垃圾回收完成後才會正常工作,這個過程GC的停頓時間可能較長,所以 – XX:CMSInitiatingOccupancyFraction的設置要根據實際的情況。
- 標記清除法有個缺點就是存在記憶體碎片的問題,那麼CMS有個參數設置-XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之後進行一次碎片整理,-XX:CMSFullGCsBeforeCompaction參數可以設置進行多少次CMS回收之後,對記憶體進行一次壓縮。
G1回收器:
- G1回收器(Garbage-First)實在jdk1.7中正式使用的垃圾回收器,從長期目標來看是為了取代CMS回收器,G1回收器擁有獨特的垃圾回收策略,G1屬於分代垃圾回收器,區分新生代和老年代,依然有eden和from/to區,它並不要求整個eden區或者新生代、老年代的空間都連續,它使用了分區演算法。
- 並行性:G1回收期間可多執行緒同時工作。
- 並發性:G1擁有與應用程式交替執行能力,部分工作可與應用程式同時執行,在整個GC期間不會完全阻塞應用程式。
- 分代GC:G1依然是一個分代的收集器,但是它是兼顧新生代和老年代一起工作,之前的垃圾收集器他們或者在新生代工作,或者在老年代工作,因此這是一個很大的不同。 空間整理:G1在回收過程中,不會像CMS那樣在若干次GC後需要進行碎片整理,G1採用了有效複製對象的方式,減少空間碎片。
- 可預見性:由於分區的原因,G1可以只選取部分區域進行回收,縮小了回收的範圍,提升了性能。
- 使用 -XX:+UseG1GC 應用G1收集器
- 使用 -XX:MaxGCPauseMillis 指定最大停頓時間
- 使用 -XX:ParallelGCThreads 設置並行回收的執行緒數量
Tomcat性能影響實驗:
- 配置環境說明: Tomcat7
- 一個JSP網站 測試網站吞吐量(1個指標、停頓時間,記憶體的使用情況,包括回收的效率….)
- 工具: Apache JMeter 下載地址://jmeter.apache.org/download_jmeter.cgi
- 實驗原理: 通過JMeter對Tomcat增加壓力,不同的虛擬機參數應該會有不同的表現
- 目的: 觀察不同配置參數對吞吐量的影響
測試串列回收器:
- -XX:+PrintGCDetails -Xmx32M -Xms32M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseSerialGC -XX:PermSize=32M
- 測試結果顯示吞吐量為:1152 115
擴大堆記憶體以提升系統性能:
- -XX:+PrintGCDetails -Xmx512M -Xms32M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseSerialGC
- -XX:PermSize=32M -Xloggc:d:/gc.log
- 測試結果顯示吞吐量為:1557 155
調整初始堆大小:
- -XX:+PrintGCDetails -Xmx512M -Xms64M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseSerialGC
- -XX:PermSize=32M -Xloggc:d:/gc.log
- 測試結果顯示吞吐量為:2100 209
測試ParNew回收器的表現:
- -XX:+PrintGCDetails -Xmx512M -Xms64M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseParNewGC -XX:PermSize=32M -Xloggc:d:/gc.log
- 測試結果顯示吞吐量為:2200 220
使用ParallelOldGC回收器:
- -XX:+PrintGCDetails -Xmx512M -Xms64M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseParallelGC -XX:+UseParallelOldGC
- -XX:ParallelGCThreads=8 -XX:PermSize=32M -Xloggc:d:/gc.log
- 測試結果顯示吞吐量為:3336 330
測試CMS回收器的性能:
- -XX:+PrintGCDetails -Xmx512M -Xms64M
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:+UseConcMarkSweepGC -XX:ConcGCThreads=8
- -XX:PermSize=32M -Xloggc:d:/gc.log
- 測試結果顯示吞吐量為:2100 209