JVM概要介紹
JVM是Java Virtual Machine(Java虛擬機)的縮寫。
虛擬機是一種抽象化的計算機,通過在實際的計算機上仿真模擬各種計算機功能來實現的。
Java虛擬機有自己完善的硬體架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。
Java虛擬機屏蔽了與具體操作系統平台相關的信息,使得Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平台上不加修改地運行,如下圖所示:
簡單來說JVM是用來解析和運行Java程序的。
JVM內存模型
首先我們來了解一下JVM的內存模型的怎么樣的:
1.堆:存放對象實例,幾乎所有的對象實例都在這里分配內存
- 堆得內存由-Xms指定,默認是物理內存的1/64;最大的內存由-Xmx指定,默認是物理內存的1/4。
- 默認空余的堆內存小於40%時,就會增大,直到-Xmx設置的內存。具體的比例可以由-XX:MinHeapFreeRatio指定
- 空余的內存大於70%時,就會減少內存,直到-Xms設置的大小。具體由-XX:MaxHeapFreeRatio指定。
2.虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息本地方法棧:本地方法棧則是為虛擬機使用到的Native方法服務。
3.方法區:存儲已被虛擬機加載的類元數據信息(元空間)
1)有時候也成為永久代,在該區內很少發生垃圾回收,但是並不代表不發生GC,在這里進行的GC主要是對方法區里的常量池和對類型的卸載
2)方法區主要用來存儲已被虛擬機加載的類的信息、常量、靜態變量和即時編譯器編譯后的代碼等數據。
該區域是被線程共享的。
3)方法區里有一個運行時常量池,用於存放靜態編譯產生的字面量和符號引用。該常量池具有動態性,也就是說常量並不一定是編譯時確定,運行時生成的常量也會存在這個常量池中。
4.程序計數器:當前線程所執行的字節碼的行號指示器
總結:
JVM垃圾回收算法
1.標記-清除: 這是垃圾收集算法中最基礎的,根據名字就可以知道,它的思想就是標記哪些要被回收的對象,然后統一回收。這種方法很簡單,但是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,導致以后程序在分配較大的對象時,由於沒有充足的連續內存而提前觸發一次GC動作。
2.復制算法: 為了解決效率問題,復制算法將可用內存按容量划分為相等的兩部分,然后每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象復制到第二塊內存上,然后一次性清楚完第一塊內存,再將第二塊上的對象復制到第一塊。但是這種方式,內存的代價太高,每次基本上都要浪費一般的內存。 於是將該算法進行了改進,內存區域不再是按照1:1去划分,而是將內存划分為8:1:1三部分,較大那份內存交Eden區,其余是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象復制到第二塊內存區上,然后清除Eden區,如果此時存活的對象太多,以至於Survivor不夠時,會將這些對象通過分配擔保機制復制到老年代中。(java堆又分為新生代和老年代)
3. 標記-整理 該算法主要是為了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了復制算法的效率問題。它的不同之處就是在清除對象的時候現將可回收對象移動到一端,然后清除掉端邊界以外的對象,這樣就不會產生內存碎片了。
4.分代收集 現在的虛擬機垃圾收集大多采用這種方式,它根據對象的生存周期,將堆分為新生代和老年代。在新生代中,由於對象生存期短,每次回收都會有大量對象死去,那么這時就采用復制算法。老年代里的對象存活率較高,沒有額外的空間進行分配擔保,所以可以使用標記-整理 或者 標記-清除。
JVM垃圾收集器有哪些?以及優劣勢比較?
1.串行Serial收集器
串行收集器是最簡單的,它設計為在單核的環境下工作(32位或者windows),你幾乎不會使用到它。它在工作的時候會暫停整個應用的運行,因此在所有服務器環境下都不可能被使用。
使用方法:-XX:+UseSerialGC
2.並行Parallel收集器
這是JVM默認的收集器,跟它名字顯示的一樣,它最大的優點是使用多個線程來掃描和壓縮堆。缺點是在minor和full GC的時候都會暫停應用的運行。並行收集器最適合用在可以容忍程序停滯的環境使用,它占用較低的CPU因而能提高應用的吞吐(throughput)。
使用方法:-XX:+UseParallelGC
3.CMS收集器
CMS是Concurrent-Mark-Sweep的縮寫,並發的標記與清除。
這個算法使用多個線程並發地(concurrent)掃描堆,標記不使用的對象,然后清除它們回收內存。在兩種情況下會使應用暫停(Stop the World, STW):
1. 當初次開始標記根對象時initial mark。
2. 當在並行收集時應用又改變了堆的狀態時,需要它從頭再確認一次標記了正確的對象final remark。
這個收集器最大的問題是在年輕代與老年代收集時會出現的一種競爭情況(race condition),稱為提升失敗promotion failure。對象從年輕代復制到老年代稱為提升promotion,但有時侯老年代需要清理出足夠空間來放這些對象,這需要一定的時間,它收集的速度可能趕不上不斷產生的要提升的年輕代對象的速度,這時就需要做STW的收集。STW正是CMS想避免的問題。為了避免這個問題,需要增加老年代的空間大小或者增加更多的線程來做老年代的收集以趕上從年輕代復制對象的速度。
除了上文所說的內容之外,CMS最大的問題就是內存空間碎片化的問題。CMS只有在觸發FullGC的情況下才會對堆空間進行compact。如果線上應用長時間運行,碎片化會非常嚴重,會很容易造成promotion failed。為了解決這個問題線上很多應用通過定期重啟或者手工觸發FullGC來觸發碎片整理。
對比並行收集器它的一個壞處是需要占用比較多的CPU。對於大多數長期運行的服務器應用來說,這通常是值得的,因為它不會導致應用長時間的停滯。但是它不是JVM的默認的收集器。
4.G1收集器
如果你的堆內存大於4G的話,那么G1會是要考慮使用的收集器。它是為了更好支持大於4G堆內存引入的。
G1之前的JVM內存模型
- 新生代:伊甸園區(eden space) + 2個幸存區
- 老年代
- 持久代(perm space):JDK1.8之前
- 元空間(metaspace):JDK1.8之后取代持久代
G1收集器的內存模型
1)G1堆內存結構
堆內存會被切分成為很多個固定大小區域(Region),每個是連續范圍的虛擬內存。
堆內存中一個區域(Region)的大小可以通過-XX:G1HeapRegionSize參數指定,大小區間最小1M、最大32M,總之是2的冪次方。
默認把堆內存按照2048份均分。
2)G1堆內存分配
每個Region被標記了E、S、O和H,這些區域在邏輯上被映射為Eden,Survivor和老年代。
存活的對象從一個區域轉移(即復制或移動)到另一個區域。區域被設計為並行收集垃圾,可能會暫停所有應用線程。
如上圖所示,區域可以分配到Eden,survivor和老年代。此外,還有第四種類型,被稱為巨型區域(Humongous Region)。Humongous區域是為了那些存儲超過50%標准region大小的對象而設計的,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
G1回收流程
在執行垃圾收集時,G1以類似於CMS收集器的方式運行。
G1收集器的階段分以下幾個步驟:
1)G1執行的第一階段:初始標記(Initial Marking )
這個階段是STW(Stop the World )的,所有應用線程會被暫停,標記出從GC Root開始直接可達的對象。
2)G1執行的第二階段:並發標記
從GC Roots開始對堆中對象進行可達性分析,找出存活對象,耗時較長。當並發標記完成后,開始最終標記(Final Marking )階段
3)最終標記(標記那些在並發標記階段發生變化的對象,將被回收)
4)篩選回收(首先對各個Regin的回收價值和成本進行排序,根據用戶所期待的GC停頓時間指定回收計划,回收一部分Region)
最后,G1中提供了兩種模式垃圾回收模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。
JVM配置參數
1)堆棧配置相關
例子:
- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
- -XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
-Xmx 3550m: 最大堆大小為3550m。
-Xms 3550m: 設置初始堆大小為3550m。
-Xmn 2g: 設置年輕代大小為2g。
-Xss 128k: 每個線程的堆棧大小為128k。
-XX:MaxPermSize: 設置持久代大小為16m
-XX:NewRatio=4: 設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。
-XX:SurvivorRatio=4: 設置年輕代中Eden區與Survivor區的大小比值。設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6
-XX:MaxTenuringThreshold=0: 設置垃圾最大年齡。如果設置為0的話,則年輕代對象不經過Survivor區,直接進入年老代。
2)垃圾收集器相關
-XX:+UseParallelGC
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC
-XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection:
-XX:+UseParallelGC: 選擇垃圾收集器為並行收集器。
-XX:ParallelGCThreads=20: 配置並行收集器的線程數
-XX:+UseConcMarkSweepGC: 設置年老代為並發收集。
-XX:CMSFullGCsBeforeCompaction:由於並發收集器不對內存空間進行壓縮、整理,所以運行一段時間以后會產生“碎片”,使得運行效率降低。此值設置運行多少次GC以后對內存空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection: 打開對年老代的壓縮。可能會影響性能,但是可以消除碎片
3)輔助信息相關
-XX:+PrintGC:開啟打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 詳細信息。
JVM調優工具
- Jconsole : jdk自帶,功能簡單,但是可以在系統有一定負荷的情況下使用。對垃圾回收算法有很詳細的跟蹤。
- JProfiler:商業軟件,功能強大。
- VisualVM:JDK自帶,功能強大,與JProfiler類似。
- MAT:MAT(Memory Analyzer Tool),一個基於Eclipse的內存分析工具。
JDK本身提供了很豐富的性能監控工具,除了集成式的visualVM和jConsole外,還有jstat,jstack,jps,jmap,jhat小工具,這些都是性能調優的常用工具。
JVM性能調優步驟

1.監控GC的狀態
使用各種JVM工具,查看當前日志,分析當前JVM參數設置,並且分析當前堆內存快照和gc日志,根據實際的各區域內存划分和GC執行時間,覺得是否進行優化。
舉一個例子: 系統崩潰前的一些現象:
- 每次垃圾回收的時間越來越長,由之前的10ms延長到50ms左右,FullGC的時間也有之前的0.5s延長到4、5s
- FullGC的次數越來越多,最頻繁時隔不到1分鍾就進行一次FullGC
- 年老代的內存越來越大並且每次FullGC后年老代沒有內存被釋放
之后系統會無法響應新的請求,逐漸到達OutOfMemoryError的臨界值,這個時候就需要分析JVM內存快照dump。
2.生成堆的dump文件
通過JMX的MBean生成當前的Heap信息,大小為一個3G(整個堆的大小)的hprof文件,如果沒有啟動JMX可以通過Java的jmap命令來生成該文件。
3.分析dump文件
打開這個3G的堆信息文件,顯然一般的Window系統沒有這么大的內存,必須借助高配置的Linux,幾種工具打開該文件:
- Visual VM
- IBM HeapAnalyzer
- JDK 自帶的Hprof工具
- Mat(Eclipse專門的靜態內存分析工具)推薦使用
備注:文件太大,建議使用Eclipse專門的靜態內存分析工具Mat打開分析。
4.分析結果,判斷是否需要優化
如果各項參數設置合理,系統沒有超時日志出現,GC頻率不高,GC耗時不高,那么沒有必要進行GC優化,如果GC時間超過1-3秒,或者頻繁GC,則必須優化。
注:如果滿足下面的指標,則一般不需要進行GC:
- Minor GC執行時間不到50ms;
- Minor GC執行不頻繁,約10秒一次;
- Full GC執行時間不到1s;
- Full GC執行頻率不算頻繁,不低於10分鍾1次;
5.調整GC類型和內存分配
如果內存分配過大或過小,或者采用的GC收集器比較慢,則應該優先調整這些參數,並且先找1台或幾台機器進行beta,然后比較優化過的機器和沒有優化的機器的性能對比,並有針對性的做出最后選擇。
6.不斷的分析和調整
通過不斷的試驗和試錯,分析並找到最合適的參數,如果找到了最合適的參數,則將這些參數應用到所有服務器。