天天看點

深入JVM

java 程式運作在 jvm 之上, jvm 的運作狀況對于 java 程式而言會産生很大的影響, 是以掌握 jvm 中的關鍵機制對于編寫穩定、 高性能的 java 程式至關重要。

jvm 制定了 java 類的加載、 編譯、 執行、 對象記憶體的配置設定和回收、 線程以及鎖機制,這些機制對 java 程式的運作效果起到了重要的影響, 當然, jvm 涉及的不僅僅是上面這些機制, 但在本章節中并不打算介紹所有 jvm 的機制, 而是僅僅深入介紹其中的一些關鍵機制。

jvm 對象記憶體回收

jvm 中自動的對象記憶體回收機制稱為: gc( garbage collection), gc 的基本原理為将記憶體中不再被使用的對象進行回收, gc 中用于回收記憶體中不被使用的對象的方法稱為收集器,由于 gc 需要消耗一些資源和時間的, java 在對對象的生命周期特征進行分析後, 在 v 1.2以上的版本采用了分代的方式來進行對象的收集, 即按照新生代、 舊生代的方式來對對象進行收集, 以盡可能的縮短 gc 對應用造成的暫停, 對新生代的對象的收集稱為 minor gc, 對舊生代的對象的收集稱為 full gc, 程式中主動調用 system.gc()強制執行的 gc 為 full gc, 在需要進行對象回收的語言( 例如還有 lisp) 中常用的有引用計數收集器和跟蹤收集器。

引用計數是辨別 heap 中對象狀态最明顯的一種方法, 引用計數的方法簡單來說就是對每一個對象都提供一個關聯的引用計數, 以此來辨別該對象是否被使用, 當這個計數為零時,說明這個對象已經不再被使用了 。

引用計數的好處是可以不用暫停應用, 當計數變為零時, 即可将此對象的記憶體空間回收,但它需要給每個對象附加一個關聯引用計數, 這就要求 jvm 在配置設定對象時必須增加指派操作, 并且引用計數無法解決循環引用的問題, 是以 jvm 并沒有采用引用計數。

跟蹤收集器的方法為停止應用的工作, 然後開始跟蹤對象, 跟蹤時從對象根開始沿着引

用跟蹤, 直到檢查完所有的對象。

jvm 的根對象集合根據實作不同而不同, 但總會包含局部變量中的對象引用和棧幀的操作數棧( 以及變量中的對象引用), 根對象的來源主要有三種。

根對象的來源之一是被加載的類的常量池中的對象引用, 例如字元串 、 被加載的類的常量池可能指向儲存在堆中的字元串 , 例如類名字, 超類名字, 超接口名字, 字段名 ,字段特征簽名 , 方法名或者方法特征簽名 。

來源之二是傳到本地方法中, 沒有被本地方法“釋放” 的對象引用。

來源之三是虛拟機運作時資料區中從垃圾收集器的堆中配置設定的部分。

跟蹤收集器采用的均為掃描的方法, 但 jvm 将 heap 分為了新生代和舊生代, 在進行minor gc 時需要掃描是否有舊生代引用了新生代中的對象, 但又不可能每次 minor gc 都掃描整個舊生代中的對象, 是以 jvm 采用了一種稱為卡片标記( card marking) 的算法來避免這種現象。

卡片标記的算法為将舊生代以某個大小(例如 512 位元組) 進行劃分, 劃分出來的每個區域稱為卡片, jvm 采用卡表維護卡的狀态, 每張卡片在卡表中占用一個位元組的辨別( 有些jvm 實作可能會不同), 當 java 代碼執行過程中發現舊生代的對象引用或釋放了對于新生代對象的引用時, 就相應的修改卡表中卡的狀态, 每次 minor gc 隻需掃描卡表中辨別為髒狀态的卡中的對象即可, 圖示如下:

深入JVM

跟蹤收集器在掃描時最重要的是要根據這些對象是否被引用來辨別其狀态, jvm 中将對象的引用分為了四種類型, 不同的對象引用類型會造成 gc 采用不同的方法進行回收:

預設情況下, 對象采用的均為強引用, 例如:

隻有當 execute 所在的這個對象的執行個體沒有其他對象引用, gc 時才會被回收。

軟引用是 java 中提供的一種比較适合于緩存場景的應用, 采用軟引用修改之上的代碼

如下:

代碼中不同于強引用中的為在 execute 方法的最後将 a 設定為了 null, 當 execute 方法執行完畢後, a 對象隻有在記憶體不夠用的情況下才會被 gc, 這對于合理的使用緩存而言無疑非常有作用, 既可以保證不至于大量使用緩存出現 outofmemory, 又可以在記憶體夠用的情況下提升性能。

采用弱引用修改之上的代碼如下:

對于 a 這個引用, 在 gc 時 a 一定會被 gc 回收, 這種引用有助于 gc 更快的回收對象,尤其是位于集合中的對象, 同時也有助于在 gc 未回收之前仍然調用此對象來執行一些動作。

采用虛引用修改之上的代碼如下:

在softreference 和 weakreference 中也可以放入 referencequeue, 這個 queue 是用于

對象在被 gc 後用于儲存 reference 對象執行個體的, 由于虛引用隻是用來得知對象是否被 gc,通過 phantomreference.get 傳回的永遠是 null, 是以它要求必須有 referencequeue, 當上面

代碼中的 a 對象被 gc 後, 通過 arefqueue.poll 可以擷取到 aref 對象執行個體, 進而可以做一些需要的動作。

在掌握了 java 中的對于根對象、 分代掃描的方式以及對象的引用類型後, 來具體的看看跟蹤收集器, 常用的有如下三種:

标記—清除( mark-sweep)

從根對象開始通路每一個活躍的節點, 并标記通路到的每一個節點, 當周遊完成後, 就對堆空間進行清除, 即清除那些沒打上标記的對象。

這種方法的好處是便于實作, 但由于要掃描整個堆, 是以要求應用暫停的時間會較長,并且會産生較多的記憶體碎片。

jvm 并沒有實作這種需要長時間停止應用的标記—清除收集器, 而是在此基礎上提供了并發的标記—清除( concurrent mark sweep, 縮寫為 cms) 收集器, 使得在整個收集的過程中隻是很短的暫停應用的執行, 可通過在 jvm 參數中設定-xx:useconcmarksweepgc 來使用此收集器, 不過此收集器僅用于舊生代和持久代的對象收集, 并發的标記 — 清除較之stop-the-world 的标記—清除複雜了很多, 來看看:并發标記—清除做到的是在标記通路每一個節點時以及清除不活躍的對象時采用和應用并發的方式, 僅需在初始化标記節點狀态以及最終标記節點狀态時需要暫停整個應用, 是以其造成的應用的暫停的時間會比較的短。

并發标記—清除為了保證盡量短的造成應用的暫停, 首先從配置設定記憶體上做了改動, cms提供了兩個 free lists, 一個用于存放小對象, 另外一個則用于存放大對象, 當 jvm 需要給對象配置設定記憶體時, 則通過 free list 來找到可用的堆位址, 并進行記憶體的配置設定以及将此位址從 freelist 删除, 當 cms 回收對象記憶體後, 則将其相應的位址重新放入此 free list 中, 這樣的好處是在回收對象的時候不需要做對象的移動等, 是以可以讓回收過程并發的進行。

接着來看看并發标記—清除的執行步驟:

1. initial marking

此步需要暫停整個應用, jvm 掃描整個 old generation 中根對象可直接通路到的對象,

并對這些對象進行标記, 對于标記的對象 cms 采用一個外部的 bit 數組來進行記錄。

2. concurrent marking

在初始化标記完畢後, cms 恢複所有應用的線程, 同時開始并發的對之前标記過的對象進行輪循, 以标記這些對象可通路的對象。cms 為了確定能夠掃描到所有的對象, 避免在 initial marking 中還有未辨別到的對象,采用的方法為找到标記了的對象, 并将這些對象放入 stack 中, 掃描時尋找此對象依賴的對象, 如果依賴的對象的位址在其之前, 則将此對象進行标記, 并同時放入 stack 中, 如依賴的對象位址在其之後, 則僅标記該對象。

在進行 concurrent marking 時 minor gc 也可能會同時進行, 這個時候很容易造成舊生代對象引用關系改變, cms 為了應對這樣的并發現象, 提供了一個 mod union table 來進行記錄, 在這個 mod union table 中記錄每次 minor gc 後修改了的 card 的資訊。

在進行 concurrent marking 時還有可能會出現的一個并發現象是應用修改了舊生代中的對象的引用關系, cms 中仍然采用 card table 的方式來進行記錄, 在 card 中将某對象辨別為 dirty 狀态, 但即使是這樣仍然可能會出現一種現象導緻不再被引用的對象仍然是 marked

的狀态:例如當 concurrent marking 已經掃描到了 a 所引用的對象 b、 c、 e, 如果在此後應用将b 引用的對象由 c 改為了 d, 同時 g 不再引用 d, 此時會将 b、 g 對象的狀态在 card 中辨別為dirty, 但 c 的狀态并不會是以而改變。

深入JVM

3. final marking

此步需要暫停整個應用, 由于在 concurrent marking 時應用可能會修改對象的引用關系或建立新的對象, 是以需要把這些改變或新建立的對象也進行掃描, cms 遞歸掃描 mod

union table 以及 card table 中 dirty 的對象, 并進行标記。

4. concurrent sweeping

在完成了 final marking 後, 恢複所有應用的線程, 就進入到這步了 , 這步需要負責的是将沒有标記的對象進行回收。

回收過程是并發進行的, 而 jvm 配置設定對象記憶體(盡管 cms 僅用于 old generation, 但有些時候會由于應用建立的對象過大導緻直接配置設定到 old generation 的現象, 另外一種現象就是 young generation 經過回收後需要轉入 old generation 的對象) 和 cms 釋放記憶體又都是操作 free list, 會産生 free list 競争的現象, 是以 cms 在此增加了 mutual exclusion locks, 以 jvm配置設定優先。

cms 為了避免每次回收後回收到的大小都比之前配置設定出去的記憶體小, 在進行 sweeping的時候, 還會盡量的将相鄰的塊重新組裝為一個塊, sweeping 為了避免和 jvm 配置設定對象記憶體産生沖突, 采用的方法為首先從 free list 中删除塊, 組裝完畢後再重新放入塊中, 為了能夠從 free list 中删除指定的塊, cms 将 free list 設計為了雙向連結清單。

cms 中的耗時的過程都是和應用并發進行的, 這也是 cms 最突出的優點, 使得其造成的應用的暫停時間比 mark-sweeping 的方式短了很多, 但同時也意味着 cms 會和應用線程争搶 cpu 資源, cms 回收記憶體的方式也使得其很容易産生記憶體碎片, 降低了空間的使用率,另外就是 cms 在回收時容易産生一些應該回收但需要等到下次 cms 才能被回收掉的對象,例如上圖中的 c 對象, 稱為“ 浮動垃圾“, 這也就要求了采用 cms 的情況下需要提供更多的可用的舊生代空間, 總體來說 cms 很适用于對響應時間要求很高、 cpu 資源競争不是很激烈以及記憶體空間相對更充足的系統。

cms 為了降低和應用争搶 cpu 資源的現象發生, 還提供了一種增量的模式, 稱為 i-cms,在這種模式下, cms 僅啟動一個處理器線程來并發的掃描标記和清除, 并且該線程在執行一小段時間後就會先将 cpu 使用權讓出來, 分多次多段的方式來完成整個掃描标記和清除的過程, 這樣降低了對于 cpu 資源的消耗, 但同時也降低了 cms 的性能, 是以僅适用于 cpu少的應用。

cms 為了減少産生的記憶體碎片, 提高 jvm 空間的使用率, 提供了一個整理碎片的功能,

可通過在 jvm 中指定-xx:+ usecmscompactatfullcollection 來啟動此功能, 在啟動了此功能後預設為每次 full gc 的時候都會進行整理, 也可以通過-xx:cmsfullgcsbeforecompaction=來指定多少次 full gc 後才執行整理, 不過要注意的是, 整理這個步驟是需要暫停整個應用的。

複制( copying)

同樣從根開始通路每一個活躍的節點, 但其不做标記, 而是将這些活動的對象複制到另

外的一個空間去, 在周遊完畢後, 隻需把原空間清空就可以了 , 過程圖示如下:

深入JVM

這種方法的好處是隻通路活躍的對象, 不用掃描整個堆中的所有對象, 是以其掃描的速度僅取決于活躍的對象的數量, 并且不會産生記憶體碎片, 但其不足的地方是需要一個同樣大小的空間, 增加了記憶體的消耗, 并且複制對象也是需要消耗時間的。

jvm 中提供了此收集器的實作, 但僅用于新生代中對象的收集, 并提供了串行和并行的兩種執行方式, 串行即為單線程運作此收集器, 可通過 -xx:+useserialgc 來指定使用串行方式的複制收集器; 并行則為多線程運作此收集器, 可通過-xx:+useparallelgc( 指定新生代、舊生代以及持久代都采用并行的方式進行收集, 舊生代并行運作收集器僅在 jdk 5 update 6後才支援) 或-xx:+useparnewgc( 指定新生代采用并行的方式進行收集) 來指定使用并行方式的複制收集器, 其中并行的線程數預設為 cpu 個數, 可通過-xx:parallelgcthreads 來指定并行運作收集器時的線程數。

複制時将 eden space 中的活躍對象和一塊 survior space 中尚不夠資格(又稱為 fromspace, 小于-xx:maxtenuringthreshold( 預設為 31 次) 次 minor gc) 進入 old generation 的活躍對象複制到另外一塊 survior space( 又稱為 to space) 中, 對于 from space 中經曆過-xx:maxtenuringthreshold 次仍然存活的對象則複制到 oldgeneration 中, 大對象也直接複制到 old generation, 如 to space 中已滿的話, 則将對象直接複制到 old generation 中(這點非 常 值 得注 意 , 在 實 際的 産 品中 要 盡量 避 免 對象 直 接 到 old generation ), 可 通 過-xx:survivorratio 來調整 survior space 所占的大小, 然後清除 eden space 和 from space, 過程圖示如下:

深入JVM

标記—整理( mark-compact)

标記—整理吸收了标記—清除和複制的優點, 第一階段從根節點周遊标記所有活躍的對象, 第二階段周遊整個堆, 清除未标記的對象, 并把存活的對象“ 壓縮“ 到堆中的一塊, 按順序排放, 這樣就避免了記憶體碎片的産生, 同時也不像複制算法需要兩倍的記憶體空間, 過程圖示如下:

深入JVM

但由于标記—整理仍然是需要周遊整個堆的, 是以其仍然要求應用暫停較長的時間。

jvm 中提供了此收集器的實作, 但僅用于舊生代中對象的收集, 同時也是舊生代預設采用的收集器, 從 jdk 5 update 6 後支援并行運作, 以加快标記—整理的執行時間, jvm 中标記—整理收集器的執行過程圖示如下:

深入JVM

java 為了降低 gc 對應用産生的影響, 一直都在不斷的發展着 gc, 并提供了多種不同的收集器, 以便 java 開發人員能夠根據硬體環境以及應用的需求來選擇相應的收集器。

并發标記—清除收集器

并發标記—清除收集器的特征是能夠讓 gc 過程暫停應用的時間縮短, 但需要消耗更多的 cpu 資源以及 jvm 記憶體空間, 并容易産生記憶體碎片, 對于響應時間要求非常靈敏的系統而言( 如 gui 系統), 是無法忍受 gc 一次帶來的幾秒的暫停的, 在這種情況下可以優先采用這種收集器。

并發标記—清除收集器僅對舊生代和持久代的收集有效, 可通過在 jvm 參數中加入-xx:useconcmarksweepgc 來采用此收集器。

串行複制收集器

此收集器僅适用于新生代的收集, 其特征為适用于快速的完成活躍對象不多的空間的收集, 不産生記憶體碎片, 但需要雙倍的記憶體空間, 執行過程中應用需要完全暫停, 可通過在jvm 參數中加入-xx:+useserialgc 來采用此收集器。

并行複制收集器

此收集器和串行複制收集器唯一不同的地方在于采用了多線程進行收集, 在超過 2 個cpu 的環境上, 其速度比串行複制收集器快很多, 是以在超過 2 個 cpu 的環境上應采用此收 集 器 來完 成 新 生代 對 象的 收 集 , 可 通 過在 jvm 參 數中 加 入 -xx:+useparallelgc 或-xx:+useparnewgc 指定使用此收集器。

串行标記—整理收集器

此收集器僅适用于舊生代的對象收集, 是 jdk 5 update 6 之前的版本中預設的舊生代收集器, 其特征為适用于收集存活時間較長的對象, 不産生記憶體碎片, 但收集造成的應用暫停的時間會比較長。

并行标記—整理收集器

此收集器和串行方式不同之處僅在于多線程執行, 是以造成的應用的暫停時間能有一定的 縮 短 , 僅 在 jdk 5 update 6 之 後 的 版 本 可 使 用 , 可 通 過 -xx:+useparallelgc 或-xx:+useparoldgc 來指定, 但不可與并發标記—整理收集器同時使用。

在 jdk 5 以前的版本中還有一個收集器是增量收集器, 此增量收集器可通過-xincgc 來啟用, 但在 jdk 5 以及以上的版本中廢棄了此增量收集器, -xincgc 會自動的轉為采用并行收集器去進行垃圾回收, 原因是其性能低于并行收集器, 是以在本書中就不介紹此收集器了 , 增量收集器中采用的火車算法比較有意思, 如果有興趣的話可以去看看。

jvm 為了避免 java 開發人員需要頭疼這麼多種收集器的選擇, 還提供了兩種簡單的方式來控制 gc 的政策:

1、 吞吐量優先

吞吐量是指 gc 所耗費的時間占應用運作總時間的百分比, 例如應用總共運作了 100 分鐘, 其中 gc 執行占用了 1 分鐘, 那麼吞吐量就是 99%了 , jvm 預設的名額是 99%。

吞吐量優先的政策即為以吞吐量為名額, 由 jvm 自行選擇相應的 gc 政策以及控制 newgeneration、 old generation 記憶體的大小, 可通過在 jvm 參數中指定-xx:gctimeratio=n 來使用此政策。

2、 暫停時間優先

暫停時間是指每次 gc 造成的應用的停頓時間, 預設不啟用這個政策。暫停時間優先的政策即為以暫停時間為名額, 由 jvm 自行選擇相應的 gc政策以及控制new generation、 old generation 記憶體的大小, 來盡量的保證每次 gc 造成的應用停頓時間都在指定的數值範圍内完成, 可通過在 jvm 參數中指定-xx:maxgcpausemillis=n 來使用此政策。

當以上兩參數都指定的情況下, 首先滿足暫停時間優先政策, 再滿足吞吐量優先政策。

大多數情況下使用預設的 jvm 配置或者使用以上兩個參數就可以讓 gc 符合應用的要求運作了 , 隻有當預設的或使用了以上兩個參數還達不到需求時, 才值得自行來調整這些和記憶體配置設定和回收相關的 jvm 參數。

在 java 中除了能夠通過調用 system.gc()來強制 jvm 進行 gc 操作外, 就隻能由 jvm 來自行決定什麼時候執行 gc 了, 由于年輕代中多數為新建立的對象, 并且大多數都已不再活躍, 是以 java 采用複制收集器來回收年輕代中的對象, 當 eden space 空間滿的時候, 會觸發 minor gc 的執行, eden space 空間滿的原因是新建立的對象的大小超過了 eden space 的大小, 例如如下的一段代碼, 當新生代的大小設定為 10m( -xmn10m), 整個 jvm 堆設定為

64m 時( -xms64m –xmx64m), 下面的代碼在執行過程中會經曆一次 minor gc:

由于新生代的大小為 10m, 那麼按照預設的 survivor ratio 為 8 的配置設定方法: eden space為 8m, 兩個 survivor space 均為 1m, 是以隻要新建立的對象超過了 8m, 就會執行 minor gc,上面的代碼中保證了 bytes 屬性中的 value 的大小在 8m, 是以可以保證在執行的過程中會經曆一次 minor gc, 按照複制收集器中的講解, 下面的程式運作狀況則會有所不同:

byte[] bytes=new byte[8*1024*1024];

這個對象會直接被配置設定到 old generation 中, 并不會觸發 minor gc, 這也是為什麼之前的一段程式中不直接配置設定大對象的原因。年老代中的對象則多數為長期活躍的對象, 是以 java 采用标記—整理收集器或并發的标記—清除收集器來回收年老代中的對象。

觸發 jvm 執行 full gc 的情況有如下兩種:

1、 old generation 空間滿或接近某個比例

old generation 空間滿的原因是從新生代提升到舊生代的對象大小+目前舊生代的對象

的大小已經接近 old generation 的空間大小, 标記—整理收集器的觸發條件為 old generation

空間滿, cms 的觸發條件為 old generation 接近某個比例。

按照之前對于複制收集器的描述, 對象從新生代提升到舊生代的原因有如下三種:

1. 新配置設定的對象的大小超過了 eden space 的大小;

2. 對象在新生代中經過了 -xx:maxtenuringthreshold 次仍然存活;

3.minor gc 時放不進 to space 中的對象;

cms 可通過-xx:cmsinitiatingoccupancyfraction 來指定舊生代中空間使用比率占到多少時, 開始執行 cms, 預設值為 68%。

當 full gc 後空間仍然不足以放入對象時, jvm 會抛出 outofmemory 的錯誤資訊, 例如下面的代碼:

當 jvm 的啟動參數設定為-xmn10m –xms18m –xmx18m 時, 上面的代碼運作會直接報出

如下錯誤:

java.lang.outofmemoryerror: java heap space

當看到這個錯誤時, 說明 jvm 的空間不足或是系統有記憶體洩露, 例如該釋放的引用沒釋放等, 但出現這個錯誤時 jvm 不一定會 crash, 伴随着的是 full gc 的頻繁執行, 會嚴重影響應用的響應速度。

2、 permanet generation 空間滿

permanet generation 中存放的為一些 class 的資訊等, 當系統中需要加載的類、 反射的類和調用的方法較多的時候, permanet generation 可能會被占滿, 占滿時如果經過 full gc仍然回收不了 , 那麼 jvm 會抛出如下錯誤資訊:

java.lang.outofmemoryerror: permgen space

當看到這個錯誤時, 說明 perm 空間配置設定的不足, 通常的解決方案為通過增大 perm 空間來解決, 配置的參數為: -xx:permsize 以及-xx:maxpermsize。

gc 仍然在繼續的發展, 除了這些已有的 gc 外, jdk 7 中增加了一種新的 garbage first的收集器, 同時 java 為了能夠滿足實時系統的要求, 還提供了一個 realtime 版的 jdk, 在這個 jdk 中允許開發人員更加靈活的控制對象的生命周期, 例如可以在某個方法中執行完畢後就自動回收對象的記憶體, 而不是等到 minor gc 或 full gc, 這兩個變化對于編寫高性能的java 應用而言都會産生不小的影響, 是以在本章節中也對其進行介紹。

garbage first

garbage first 簡稱 g1, 它的目标是要做到盡量減少 gc 所導緻的應用暫停的時間, 讓應用達到準實時的效果, 同時保持 jvm 堆空間的使用率, 其最大的特色在于允許指定在某個時間段内 gc 所導緻的應用暫停的時間最大為多少, 例如在 100 秒内最多允許 gc 導緻的應用暫停時間為 1 秒, 這個特性對于準實時響應的系統而言非常的吸引人, 這樣就再也不用擔心系統突然會暫停個兩三秒了。

g1 要做到這樣的效果, 也是有前提的, 一方面是硬體環境的要求, 必須是多核的 cpu以及較大的記憶體( 從規範來看, 512m 以上就滿足條件了 ), 另外一方面是需要接受吞吐量的稍微降低, 對于實時性要求高的系統而言, 這點應該是可以接受的。

為了能夠達到這樣的效果, g1 在原有的各種 gc 政策上進行了吸收和改進, 在 g1 中可以看到增量收集器和 cms 的影子, 但它不僅僅是吸收原有 gc 政策的優點, 并在此基礎上做出了很多的改進, 簡單來說, g1 吸收了增量 gc 以及 cms 的精髓, 将整個 jvm heap 劃分為多個固定大小的 region, 掃描時采用 snapshot-at-the-beginning 的并發 marking 算法( 具體在後面内容詳細解釋) 對整個 heap 中的 region 進行 mark, 回收時根據 region 中活躍對象的bytes 進行排序, 首先回收活躍對象 bytes 小以及回收耗時短( 預估出來的時間) 的 region,回收的方法為将此 region 中的活躍對象複制到另外的 region 中, 根據指定的 gc 所能占用的時間來估算能回收多少 region, 這點和以前版本的 full gc 時得處理整個 heap 非常不同, 這樣就做到了能夠盡量短時間的暫停應用, 又能回收記憶體, 由于這種政策在回收時首先回收的是垃圾對象所占空間最多的 region, 是以稱為 garbage first。看完上面對于 g1 政策的簡短描述, 并不能清楚的掌握 g1, 在繼續詳細看 g1 的步驟之前, 必須先明白 g1 對于 jvm heap 的改造, 這些對于習慣了劃分為 new generation、 old generation 的大家來說都有不少的新意。

g1 将 heap 劃分為多個固定大小的 region, 這也是 g1 能夠實作控制 gc 導緻的應用暫停時間的前提, region 之間的對象引用通過 remembered set 來維護, 每個 region 都有一個remembered set, remembered set 中包含了引用目前 region 中對象的 region 的對象的 pointer,由于同時應用也會造成這些 region 中對象的引用關系不斷的發生改變, g1 采用了 card table來用于應用通知 region 修改 remembered sets, card table 由多個 512 位元組的 card 構成, 這些 card 在 card table 中以 1 個位元組來辨別, 每個應用的線程都有一個關聯的 remembered set log, 用于緩存和順序化線程運作時造成的對于 card 的修改, 另外, 還有一個全局的 filled rs buffers, 當應用線程執行時修改了 card 後, 如果造成的改變僅為同一 region 中的對象之間的關聯, 則不記錄 remembered set log, 如造成的改變為跨 region 中的對象的關聯, 則記錄到線程的 remembered set log, 如線程的 remembered set log 滿了 , 則放入全局的 filled rs buffers 中, 線程自身則重新建立一個新的 remembered set log, remembered set 本身也是一個由一堆 cards 構成的哈希表。

盡管 g1 将 heap 劃分為了多個 region, 但其預設采用的仍然是分代的方式, 隻是僅簡單的劃分為了年輕代( young) 和非年輕代, 這也是由于 g1 仍然堅信大多數新建立的對象都是不需要長的生命周期的, 對于應用新建立的對象, g1 将其放入辨別為 young 的 region中, 對于這些 region, 并不記錄 remembered set logs, 掃描時隻需掃描活躍的對象, g1 在分代的方式上還可更細的劃分為: fully young 或 partially young, fully young 方式暫停的時候僅處理 young regions, partially 同樣處理所有的 young regions, 但它還會根據允許的 gc 的暫停時間來決定是否要加入其他的非 young regions, g1 是運作到 fully-young 方式還是partially young 方式, 外部是不能決定的, 在啟動時, g1 采用的為 fully-young 方式, 當 g1完成一次 concurrent marking 後, 則切換為 partially young 方式, 随後 g1 跟蹤每次回收的效率, 如果回收 fully-young 中的 regions 已經可以滿足記憶體需要的話, 那麼就切換回 fully young方式, 但當 heap size 的大小接近滿的情況下, g1 會切換到 partially young 方式, 以保證能提供足夠的記憶體空間給應用使用。

除了分代方式的劃分外, g1 還支援另外一種 pure g1 的方式, 也就是不進行代的劃分,pure 方式和分代方式的具體不同在下面的具體執行步驟中進行描述。

掌握了這些概念後, 繼續來看 g1 的具體執行步驟:

g1 對于每個 region 都儲存了兩個辨別用的 bitmap, 一個為 previous marking bitmap, 一個為 next marking bitmap, bitmap 中包含了一個 bit 的位址資訊來指向對象的起始點。

開始 initial marking 之前, 首先并發的清空 next marking bitmap, 然後停止所有應用線程,并掃描辨別出每個 region 中 root 可直接通路到的對象, 将 region 中 top 的值放入 next top at mark start( tams) 中, 之後恢複所有應用線程。

觸發這個步驟執行的條件為:

g1 定義了一個 jvm heap 大小的百分比的閥值, 稱為 h, 另外還有一個 h, h 的值為(1-h)*heap size, 目前這個 h 的值是固定的, 後續 g1 也許會将其改為動态的, 根據 jvm 的運作情況來動态的調整, 在分代方式下, g1 還定義了一個 u 以及 soft limit,

soft limit 的值為 h-u*heap size, 當 heap 中使用的記憶體超過了 soft limit 值時, 就會在一次 clean up 執行完畢後在應用允許的 gc 暫停時間範圍内盡快的執行此步驟;

在 pure 方式下, g1 将 marking 與 clean up 組成一個環, 以便 clean up 能充分的使用 marking 的資訊, 當 clean up 開始回收時, 首先回收能夠帶來最多記憶體空間的

regions, 當經過多次的 clean up, 回收到沒多少空間的 regions 時, g1 重新初始化一個新的 marking 與 clean up 構成的環。

按照之前 initial marking 掃描到的對象進行周遊, 以識别這些對象的下層對象的活躍狀

态, 對于在此期間應用線程并發修改的對象的以來關系則記錄到 remembered set logs 中,

新建立的對象則放入比 top 值更高的位址區間中, 這些新建立的對象預設狀态即為活躍的,同時修改 top 值。

3. final marking pause

當應用線程的 remembered set logs 未滿時, 是不會放入 filled rs buffers 中的, 在這樣的情況下, 這些 remebered set logs 中記錄的 card 的修改就會被更新了 , 是以需要這一步, 這一步要做的就是把應用線程中存在的 remembered set logs 的内容進行處理, 并相應的修改remembered sets, 這一步需要暫停應用, 并行的運作。

4. live data counting and cleanup

值得注意的是, 在 g1 中, 并不是說 final marking pause 執行完了, 就肯定執行 cleanup這步的, 由于這步需要暫停應用, g1 為了能夠達到準實時的要求, 需要根據使用者指定的最大的 gc 造成的暫停時間來合理的規劃什麼時候執行 cleanup, 另外還有幾種情況也是會觸發這個步驟的執行的:

g1 采用的是複制方法來進行收集, 必須保證每次的”to space”的空間都是夠的, 是以 g1 采取的政策是當已經使用的記憶體空間達到了 h 時, 就執行 cleanup 這個步驟;

對于 full-young 和 partially-young 的分代模式的 g1 而言, 則還有情況會觸發 cleanup的執行, full-young 模式下, g1 根據應用可接受的暫停時間、 回收 young regions需要消耗的時間來估算出一個 yound regions 的數量值, 當 jvm 中配置設定對象的 young regions 的數量達到此值時, cleanup 就會執行; partially-young 模式下, 則會盡量頻繁的在應用可接受的暫停時間範圍内執行 cleanup , 并最大限度的去執行non-young regions 的 cleanup。這一步中 gc 線程并行的掃描所有 region, 計算每個 region 中低于 next tams 值中 marked data 的大小, 然後根據應用所期望的 gc 的短延時以及 g1 對于 region 回收所需的耗時的預估, 排序 region, 将其中活躍的對象複制到其他 region 中。g1 為了能夠盡量的做到準實時的響應, 例如估算暫停時間的算法、 對于經常被引用的對象的特殊處理等, g1 為了能夠讓 gc 既能夠充分的回收記憶體, 又能夠盡量少的導緻應用的暫停, 可謂費盡心思, 從 g1 的論文中的性能評測來看效果也是不錯的, 不過如果 g1 能允許開發人員在編寫代碼時指定哪些對象是不用 mark 的就更完美了 , 這對于有巨大緩存的應用而言, 會有很大的幫助, g1 随 jdk 6 update 14 已經 beta 釋出, 在 java 7 中估計會正式的作為替代 cms 的 gc 政策, 由于在本書的編寫階段中 g1 尚處于 beta 階段, 不過還是嘗嘗鮮,來看看 g1 的實際表現吧。

real-time 版的 jdk

為了滿足實時領域系統使用 java 的需求, 也為了讓 java 能夠進入更多的高端領域, java推出了 real-time 版的規範( jsr-001, 更新的版本為 jsr-282), 并且各大廠商也都積極響應,相應的推出了 real-time 實作的 jdk, real-time 版的 jdk 對 java 做出了很多的改進, 例如強大的線程排程機制、 異步的事件處理機制、 更為精準的時間刻度等, 在此最為關心的是其在java 記憶體管理方面的加強。gc 無疑是 java 進入實時領域的一個很大的障礙, 畢竟無論 gc 怎麼改進, 它肯定是會造成應用暫停的現象的, 而且是在運作時突然的就會造成暫停, 這對于實時系統來說是不可接受的, 是以 real-time 版的 jdk 在此方面做出了多方面的改進, 由于沒試用過, 在此也隻能是按照規範紙上談兵了 。

新的記憶體管理機制

提供了兩種記憶體區域: immortal 記憶體區域和 scoped 記憶體區域。

immortal 記憶體區域用于保留永久的對象, 這些對象僅在應用結束運作時才會釋放記憶體,這個最典型的需求場景莫過于緩存了 。

scoped 記憶體區域用于保留臨時的對象, 位于 scope 中的對象在 scope 退出時, 這些對象所占用的記憶體會被直接回收。

immortal 記憶體區域和 scoped 記憶體區域均不受 gc 管理, 是以基于這兩個記憶體區域來編寫的應用完全不用擔心 gc 會造成暫停的現象。

允許 java 應用直接通路實體記憶體

在保證安全的情況下, real-time jdk 允許 java 應用直接通路實體記憶體, 而非像以前的java 程式, 需要通過 native code 才能通路, 能夠通路實體記憶體, 也就意味着可以直接将對象放入實體記憶體, 而非 jvm heap 中。

jvm 記憶體狀況檢視和分析工具

java 本身提供了多種豐富的工具來幫助開發人員檢視和分析 gc 以及 jvm 記憶體的狀況,同時開源界和商業界也有一些工具可用于檢視、 分析 gc 以及 jvm 記憶體的狀況, 通過這些分析可以來排查程式中記憶體洩露的問題以及調優程式的性能, 在下面介紹幾種常用的免費工具,商業工具就不在此處介紹了 , 其中知名的有 jprofiler 等。

輸出 gc 日志對于跟蹤分析 gc 的狀況, 無疑是最明顯和直接的分析記憶體回收狀況的方法, 隻是 gc 日志輸出後需要人肉的進行分析, 來判斷 gc 的狀況。

jvm 支援将日志輸出到控制台或指定的檔案中, 方法為:

在 jvm 的啟動參數中加入 -xx:+printgc -xx:+printgcdetails -xx:+printgctimestamps-xx:+printgcapplicationstoppedtime, 按照參數的順序分别可以輸出 gc 的簡要資訊, gc 的詳細資訊、 gc 的時間資訊以及 gc 造成的應用暫停的時間。

在 1 中的 jvm 啟動參數中再增加-xloggc: gc.log 可指定将 gc 的資訊輸出到 gc.log 中。所輸出的 gc 日志會由于采用的 jdk 版本以及 gc 政策有所不同, 在 jdk 1.6.0 的環境中增加了以上參數後會打出類似如下的 gc 日志資訊:

gc portal

将 gc 日志輸出固然有一定的作用, 但如果要靠人肉進行分析的話還是相當複雜的, 是以 sun 提供了一個 gc portal 來幫助分析這些 gc 日志, 并生成相關的圖形化的報表, gc portal部署起來會有些麻煩, 它需要運作在老版本的 tomcat 上, 同時需要資料庫, 部署完畢後通過上傳日志檔案的方式即可完成 gc 日志的分析, 此 gc 日志輸出的 jvm 參數使用的為:

在上傳日志時 gc portal 的選項裡隻有 jdk 1.2 或 jdk 1.2—1.4 的版本, 但經過測試, jdk 6 的日志也是可以分析出來的, 但它的限制在于僅支援 5mb 的 gc 日志的分析, gc portal 可提供吞吐量的分析、 耗費的 cpu 的時間、 造成的應用暫停的時間、 每秒從新生代轉化到舊生代的數量、 minor gc的狀況以及 full gc 的狀況等, 圖示如下:

深入JVM

gc portal 中還有一個比較有意思的部分是提供調整 gc 參數的預測, 例如可以選擇給young size 增加 20%的空間, gc portal 會根據目前的日志資訊來評估在調整參數後的運作效果, 雖然不一定準, 但确實還是有些參考意義的。

jconsole

jconsole 可以圖形化的檢視 jvm 中記憶體的 gc 狀況, 可以很容易的從圖形中看出 gc 的變化狀況, jconsole 是 jdk 5 及以上的版本中自帶的工具, 位于 jdk 的 bin 目錄下, 運作時直接運作 jconsole.exe 或 jconsole.sh( 要求支援圖形界面), 在本地的 tab 頁上看到運作了java 的 pid, 輕按兩下即可檢視相應程序的 jvm 的狀況, 同時, jconsole 也支援檢視遠端的 jvm的運作狀況, 具體可參見 jconsole 的 user guide。

jconsole 中顯示了 jvm 中很多的資訊: 記憶體、 線程、 類和 mbean 等, 在打開 jconsole的記憶體 tab 頁後, 可看到 jvm 記憶體部分的運作狀況, 這對于分析記憶體是否有溢出以及 gc 的效果能夠更直接明了的看出來, 在性能調優章節中将再次使用此工具, 并進行更加詳細的解釋, jconsole 的運作效果圖示如下:

深入JVM

jvisualvm

jvisualvm 是 jdk 6 update 7 之後推出的一個工具, 此工具可以看做是一個類似 jprofiler的工具, 基于此工具可檢視記憶體的消耗情況、 線程的執行狀況以及程式中消耗 cpu、 記憶體的動作。

在記憶體方面的分析上, jvisualvm 帶來的最大的好處是其可通過安裝 visualgc 插件來分析 gc 趨勢、 記憶體消耗詳細狀況。

visualgc 的運作圖示如下:

深入JVM

在上面的圖中可看到各區的記憶體消耗狀況以及 gc time 的圖表, 而其提供的 histogram視圖對于調優也有很大的幫助。

基于 jvisualvm 的 profiler 中的 memory 則可檢視對象占用記憶體的狀況, 圖示如下:

jmap

jmap 是 jdk 中自帶的一個用于分析 jvm 記憶體狀況的工具, 位于 jdk 的 bin 目錄下, 使用 jmap 可檢視目前 jvm 中各個代的記憶體狀況, jvm 中對象的記憶體的占用狀況、 導出整個 jvm

中的記憶體資訊。

在 linux 上直接通過 jmap [pid], 就可檢視整個 jvm 中記憶體的狀況, 看到的資訊類似如下( 和 jdk 版本、 gc 政策有關):

從上面的資訊中可看出 jvm 堆的配置資訊, 例如 newsize、 newratio、 survivorratio 等;

jvm 堆的使用情況, 例如新生代中的 eden space、 from space、 to space 的使用情況、 舊生代和持久代的使用情況。

在檢視 jvm 記憶體狀況時, 除了需要知道每個代的占用情況外, 很多時候更想知道的是其中各個對象占用的記憶體大小, 這樣便于分析對象的記憶體占用的情況, 在分析 outofmemory的場景中尤其适用。

輸入 jmap –histo [pid]即可檢視到 jvm 堆中對象的詳細占用情況, 類似如下:

深入JVM

輸出的内容按照占用的空間的大小排序, 例如上面的[c, 表示的是 char 類型的對象在jvm 中總共有 243707 個執行個體, 占用了 501638784 bytes 的空間。

通過上面的方法能檢視到 jvm 中對象記憶體的占用情況, 确實已經不錯了 , 但很多時候還需要知道這個對象到底是誰建立的, 例如上面顯示出來的[c, 隻能知道它占用了那麼多的空間, 但不知道是誰建立出的[c, jmap 也想到了這點, 于是提供了導出整個 jvm 中的記憶體資訊的支援, 基于一些 jvm 記憶體的分析工具, 例如 sun jdk 6 中的 jhat、 eclipse memory analyzer,可以分析 jvm 中記憶體的詳細資訊, 例如[c 是哪些對象建立的。

執行如下指令即可導出整個 jvm 中記憶體資訊:

jmap -dump:format=b,file=檔案名 [pid]

jhat

jhat 是 sun jdk 6 及以上版本中自帶的一個用于分析 jvm 堆 dump 檔案的工具, 基于此工具可分析 jvm heap 中對象的記憶體占用狀況、 引用關系等。

執行如下指令分析 jvm 堆的 dump 檔案:

jhat –j-xmx1024m [file]

執行後等待 console 中輸出 started http server on port 7000, 看到這個後就可以通過浏覽器通路 http://ip:7000 了 , 此頁面預設為按 package 分類顯示系統中所有的對象執行個體, 在

頁面的最下端有 other queries 導航, 其中有顯示 jvm 中對象執行個體個數的連結、 有顯示 jvm中對象大小的連結等, 點選顯示 jvm 中對象大小的連結, 得到的結果圖示如下:

深入JVM

點選上圖中的 class [c, 可以看到有哪些對象執行個體引用了這個對象, 或者建立了這個對象, 總體來說 jhat 還是不錯的, 不過 jhat 在分析大的堆 dump 檔案時表現的不好, 速度很慢。

jstat

jstat 是 sun jdk 自帶的一個統計分析 jvm 運作狀況的工具, 位于 jdk 的 bin 目錄下, 除了可用于分析 gc 的狀況外, 還可用于分析編譯的狀況、 class 加載的狀況等。

jstat 用于 gc 分析的參數有:

<code>-gc、 -gccapacity、 -gccause、 -gcnew、 -gcnewcapacity、 -gcold、-gcoldcapacity、 -gcpermcapacity、 -gcutil</code> , 常用的為-gcutil, 通過-gcutil 可按一定頻率檢視 jvm中各代的空間的占用情況、 minor gc 的次數、 消耗的時間、 full gc 的次數以及消耗的時間的統計, 執行 jstat –gcutil [pid] [interval], 可看到類似如下的輸出資訊:

其中 s0、 s1 就是 survivor 空間的使用率, e 表示 eden 空間的使用率, o 表示舊生代空間的使用率, p 表示持久代的使用率, ygc 表示 minor gc 的執行次數, ygct 表示 minor gc執行消耗的時間, fgc 表示 full gc 的執行次數, fgct 表示 full gc 執行消耗的時間, gct 表示 minor gc+full gc 執行消耗的時間。

eclipse memory analyzer

eclipse memory analyzer 是 eclipse 提供的一個用于分析 jvm 堆 dump 檔案的插件, 借助這個插件可用于檢視對象的記憶體占用狀況、 引用關系、 分析記憶體洩露等。

在 eclipse 中可以直接遠端安裝此插件, 不過由于此插件在分析堆 dump 檔案時比較耗記憶體, 是以在分析前最好先将 eclipse 的 jvm 的記憶體設定大一點, mat 分析 dump 檔案後的對象占用記憶體以及引用關系圖示如下:

深入JVM

mat 還是非常不錯的, 相對而言功能比 jhat 強大很多, 分析的速度也快一些, 是以如果需要分析 jvm 堆 dumap 檔案, 首選推薦的還是 mat。