什麼是Java記憶體回收行程
Java記憶體回收行程是Java虛擬機器(JVM)的三個重要模組(另外兩個是解譯器和多線程機制)之一,為應用程式提供記憶體的自動分配(Memory Allocation)、自動回收(Garbage Collect)功能,這兩個操作都發生在Java堆上(一段記憶體快)。某一個時點,一個對象如果有一個以上的引用(Rreference)指向它,那麼該對象就為活著的(Live),否則死亡(Dead),視為垃圾,可被記憶體回收行程回收再利用。記憶體回收操作需要消耗CPU、線程、時間等資源,所以容易理解的是記憶體回收操作不是即時的發生(對象死亡馬上釋放),當記憶體消耗完或者是達到某一個指標(Threshold,使用記憶體佔總記憶體的比列,比如0.75)時,觸發記憶體回收操作。有一個對象死亡的例外,java.lang.Thread
類型的對象即使沒有引用,只要線程還在運行,就不會被回收。
回收的機制
依據統計分析可知,Java(包括一些其它進階語言)裡面大多數對象生命週期都是短暫的,所以把Java記憶體分代管理。分代的目的無非就是為不同代的記憶體塊運用不同的管理原則(演算法),從而最大化效能。相對於年老代,通常年輕代要小很多,回收的頻率高,速度快。年老代則回收頻率低,耗時間長度。記憶體在年輕代裡面分配,年輕代裡面的對象經過多個回收周期依然存活的會自動晉陞到年老代。
設計選型(Design Choices)
設計選型影響JVM記憶體回收行程的實現難度,以及JVM的效能指標,適用於不同的情境。描述的是回收演算法的風格特點。
單線程串列回收 VS 多線程並行回收
回收操作自身是否多執行緒的問題。單線程回收的優點是簡單,易實現,片段少,適用於單核的機器。多線程並行回收在多核機器上面可以充分的利用CPU資源,減少回收的時間,增加生產力,缺點是複雜且可能有部分片段沒有回收。
回收時暫停應用線程 VS 回收和應用並發進行
回收操作時是否暫停應用線程的問題。暫停應用線程的優點是簡單、準確、清理得比較乾淨、清理的時間也短(CPU資源獨佔),缺點是暫停應用線程之後會造成記憶體回收周期內應用的回應時間拉長,即時性非常高的系統比較敏感。回收和應用線程平行處理的優點是應用反應時間比較平穩、缺點是實現難度大、清理頻率高、可能有片段。
不合并釋放的記憶體片段 VS 合并釋放的記憶體片段 VS 把活著的複製到新的地方
這三個選型描述的是如何管理死亡的記憶體塊片段。死亡的記憶體片段通常散落在堆的各個地方,如果不加以管理會有兩個問題,記憶體配置的時候因尋找可用的記憶體而導致速度慢,小的片段會導致記憶體的浪費(比如大的數組要求大的連續記憶體片段)。管理有兩種方式,把活著的記憶體挪到記憶體塊的某一端,記錄可用記憶體的開始位置,或者乾脆把活著的記憶體複製到一個新的記憶體地區,原來的記憶體塊整個空出來。
效能指標(Performance Metrics)
- 生產率(Throughput)
一個較長的周期(長的周期才有意義)內,非回收時間佔總時間的比率。度量系統的運行效率。
- 記憶體回收花費(Garbage Collection overhead)
一個較長的周期內,回收時間佔總時間的比率。與生產率相對應,加起來為100%。
- 暫停時間間隔(Pause time)
Java虛擬機器在回收垃圾的時候,有的演算法會暫停所有應用線程的執行,某些系統可能對暫停時間間隔比較敏感。
- 回收的頻率(Frequency of collection)
平均多久會發生回收操作。
- 記憶體佔用的大小(Footprint)
如堆的大小。
- 即時性(Promptness)
自一個對象死亡起,經過多久該對象所佔用記憶體被回收。
記憶體回收的類型
所有的回收器類型都是基於分代技術。Java HotSpot虛擬機器包含三代,年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation)。
- 永久代
儲存類、方法以及它們的描述資訊。可以通過-XX:PermSize=64m
和-XX:MaxPermSize=128m
兩個可選項指定初始大小和最大值。通常 我們不需要調節該參數,預設的永久代大小足夠了,不過如果載入的類非常多,不夠用了,調節最大值即可。
- 年老代
主要儲存年輕代中經過多個回收周期仍然存活從而升級的對象,當然對於一些大的記憶體配置,可能也直接分配到永久代(一個極端的例子是年輕代根本就存不下)。
- 年輕代
絕大多數的記憶體配置回收動作都發生在年輕代。如所示, 年輕代被劃分為三個地區,原始區(Eden)和兩個小的存活區(Survivor),兩個存活區按功能分為From和To。絕大多數的對象都在原始區分配,超過一個記憶體回收操作仍然存活的對象放到存活區。
串列回收器(Serial Collector)
單線程執行回收操作,回收期間暫停所有應用線程的執行,client模式下的預設回收器,通過-XX:+UseSerialGC
命令列可選項強制指定。
- 年輕代的回收演算法(Minor Collection)
把Eden區的存活對象移到To區,To區裝不下直接移到年老代,把From區的移到To區,To區裝不下直接移到年老代,From區裡面年齡很大的升級到年老代。 回收結束之後,Eden和From區都為空白,此時把From和To的功能互換,From變To,To變From,每一輪迴收之前To都是空的。設計的選型為複製。
- 年老代的回收演算法(Full Collection)
年老代的回收分為三個步驟,標記(Mark)、清除(Sweep)、合并(Compact)。標記階段把所有存活的對象標記出來,清除階段釋放所有死亡的對象,合并階段 把所有活著的對象合并到年老代的前部分,把閒置片段都留到後面。設計的選型為合并,減少記憶體的片段。
並行回收器(Parallel Collector)
使用多個線程同時進行記憶體回收,多核環境裡面可以充分的利用CPU資源,減少回收時間,增加JVM生產率,Server模式下的預設回收器。與串列回收器相同,回收期間暫停所有應用線程的執行。通過-XX:+UseParallelGC
命令列可選項強制指定。
- 年輕代的回收演算法(Minor Collection)
使用多個線程回收垃圾,每一個線程的演算法與串列回收器相同。
- 年老代的回收演算法(Full Collection)
年老代依然是單線程的,與串列回收器相同。
並行合并收集器(Parallel Compacting Collection)
年輕代和年老代的回收都是用多執行緒。通過命令可選項-XX:+UseParallelOldGC
指定,–XX:ParallelGCThreads=3
還可進一步指定參與並行回收的線程數。與串列回收器相同,回收期間暫停所有應用線程的執行。與並行回收器相比,年老代的回收時間更短,從而減少了暫停時間間隔(Pause time)。通過–XX:+UseParallelOldGC
命令列可選項強制指定。
- 年輕代的回收演算法(Minor Collection)
與並行回收器(Parallel Collector)相同
- 年老代的回收演算法(Full Collection)
年老代分為三個步驟,標記、統計、合并。這裡用到分的思想,把年老代劃分為很多個固定大小的區(region)。 標記階段,把所有存活的對象劃分為N組(應該與回收線程數相同),每一個線程獨立的負責自己那一組,標記存活對象的位置以及 所在區(Region)的存活率資訊,標記為並行的。統計階段,統計每一個區(Region)的存活率,原則上靠前面的存活率較高,從前到後, 找到值得合并的開始位置(絕大多數對象都存活的區不值得合并),統計階段是串列的(單線程)。合并階段,依據統計階段的資訊,多線程 並行的把存活的對象從一個區(Region)複製到另外一個區(Region)。
並發標記清除回收器(Concurrent Mark-Sweep Collector)
又名低延時收集器(Low-latency Collector),通過各種手段使得應用程式被掛起的時間最短。基本與應用程式並發地執行回收操作,沒有合并和複製操作。通過命令列-XX:+UseConcMarkSweepGC
指定,在單核或者雙核系統裡面還可以指定使用增量式回收模式-XX:+UseConcMarkSweepGC
。增量式回收是指把回收操作分為多個片段,執行一個片段之後釋放CPU資源給應用程式,未來的某個時點接著上次的結果繼續回收下去。目的也是減少延時。
- 年輕代的回收演算法(Minor Collection)
與並行回收器(Parallel Collector)相同
- 年老代的回收演算法(Full Collection)
分為四個步驟,初始標記(Initial Mark)、並發標記(Concurrent Mark)、再次標記(Remark)、以及並發清理(Concurrent Sweep)。特別注意,沒有合併作業,所以會有片段。
- 初始化階段: 暫停應用線程,找出所有存活的對象,耗時比較短,回收器使用單線程。
- 並發標記階段: 回收器標記操作與應用並發運行,回收器使用單線程標記存活對象。
- 再次標記:並發標記階段由於應用程式也在運行,這個過程中可能新增或者修改對象。所以再次暫停應用線程,找出所有修改的對象,使用多線程標記。
- 並發清理:回收器清理操作與應用並發運行,回收器使用單線程清理死亡對象。
Java記憶體回收行程的效能評估工具
–XX:+PrintGCDetails
和–XX:+PrintGCTimeStamps
記憶體回收的開始時間,期間,每一代的空餘記憶體等資訊。
- jmap [options] pid
jamp 2043 查看2043進程裡面已經載入的共用對象。通常DLL檔案。
jmap -heap 2043 查看記憶體堆的配置資訊以及使用方式。
jmap -permstat 2043 查看永久代的載入情況。
jmap -histo 2043 查看類的載入和記憶體佔用情況。
- jstat [options] pid
jstat -class 2043 class載入、卸載、記憶體佔用情況。
jstat -gc 2043 GC執行情況。
後記
Java提供自動選擇和自動效能最佳化功能。在做記憶體回收行程調優之前,先列出所關注的效能指標,通過命令列告訴JVM你所關注的效能指標,由JVM自動調優,如果不滿意,可以指定記憶體回收行程。OutOfMemory通常是由於堆記憶體不足,調節-Xmx1024m
和-XX:MaxPermSize=128m
命令列可選項即可。
參考連結
Java Memory Management Whitepaper