2012年6月30日 星期六

[ JVM 應用 ] 垃圾收集器與記憶體分配策略 - Part2

垃圾收集演算法 : 
在 Part1 我們介紹了 JVM 如何確認物件存活使用的演算法, 當確定某物件不再被參考到或存活, 接著便是要把它們當作垃圾收集摟. 這邊介紹幾種常見的垃圾收集演算法. 

- 標記/清除 演算法 
最基礎的收集演算法便是 "標記-清除" (Mark-Sweep) 演算法, 如它的名字一樣, 演算法分為 "標記" 和 "清除" 兩個階段 : 第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片. 示意圖如下 : 
 

- 複製演算法 
此 演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域. 垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中. 演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不過出現 "碎片" 問題. 當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間 : 
 

- 標記/整理 演算法 
複製演算法在物件存活率較高的場合需要進行較多的複製操作, 效率將打折扣. 更關鍵的是如果不想浪費 50% 空間, 就需要有額外空間進行分配擔保, 以應對使用的記憶體所有物件都 100% 存活的極端狀況, 所以在老年代一般不會採用這種演算法. 而 "標記-整理" 演算法結合了 "標記-清除" 和 "複製" 兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個 Heap,清除未標記物件並且把存活物件 "壓縮" 到堆的其中一塊並按順序排放。此演算法避免了"標記-清除" 的碎片問題,同時也避免了 "複製" 演算法的空間問題. 

- 分代演算法(Generational Collecting) 
基於對物件生命週期分析後得出的垃圾回收演算法。把物件分為年青代(Young)、年老代(Tenured) 與 持久代(Perm),對不同生命週期的物件使用不同的演算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此演算法的。 

垃圾收集器 : 
如果說收集演算法是記憶體回收的方法論, 垃圾收集器就是記憶體回收的具體實現. JVM 規範中對 垃圾收集器 如何實現並沒有任何規定, 因此不同廠商, 不同版本的 JVM 的 垃圾收集器可能會有不小的差別, 並且一般都會提供參數提供使用者根據自己的應用特點與要求組合出各個年代所使用的收集器. 底下是 Sun HotSpot JVM 1.6 update 22 所包含的所有收集器 :  

- Serial 收集器/串列收集器 
使用單執行緒所有垃圾回收工作,因為無需多執行緒交互,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單一處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用 -XX:+UseSerialGC 打開. 
 

- ParNew 收集器 
ParNew 收集器其實就是 Serial 收集器的多執行序版本, 除了使用多執行序進行垃圾收集外, 你也可以透過參數 -XX:ParallelGCThreads 控制其開啟的線程數. ParNew 收集器在單 CPU 環境絕對不會有比 Serial 收集器更好的效能, 由於其在執行序間互動的開銷, 甚至在兩個 CPU 都不能保證超越 Serial 收集器. 當然隨著 CPU 數量增加, 自然效能會越來越好. 
 

- Parallel Scavenge 收集器 
Parallel Scavenge 是一個新生代收集器, 使用複製演算法也是平行的多執行緒收集器 (與 ParNew 沒兩樣?). 它的特點是它提供參數讓你可以透過參數設定控制 "傳輸量" 的能力. 因此你可以決定進行 GC 時的停頓時間. 所謂 "傳輸量" (Throughput) 即是 : 
傳輸量 = 執行用戶程式碼時間 / (執行用戶程式碼時間 + 垃圾收集時間)

考慮 JVM 共執行 100 分鐘, 其中 GC 回收花掉 1 分鐘, 那傳輸量就是 99%. 停頓時間越短就越適合需要與使用者互動的程式, 而高傳輸量可以高效率地利用 CPU 時間完成運算任務而減少使用者等待時間. Parallel Scavenge 收集器提供兩個參數用於精準控制傳輸量, 分別是控制最大 GC 停頓時間的 -XX:MaxGCPauseMillis (>0) 與直接設置傳輸量大小的 -XX:GCTimeRatio 參數 (>0 && <100). 除了上述兩個參數外, Parallel Scavenge 還有一個參數 -XX:+UseAdaptiveSizePolicy 值得關注. 這是一個開關參數, 當打開後就不需要手動指定新生代的大小 (-Xmn) , Eden 與 Survivor 區的比例 -XX:SurvivorRatio 等細節參數, JVM 會自動根據當時系統地執行狀態, 收集性能資訊並動態的自我調整 (GC Ergonomics). 

- Serial Old 收集器 
Serial Old 是 Serial 收集器的老年代版本, 它同樣也是一個單執行序收集器. 使用 "標記-整理" 演算法. 

- Parallel Old 收集器 
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多執行序和 "標記-整理" 演算法. 這個收集器是在 JDK 1.6 中才開始提供. 在注重傳輸量與 CPU 資源敏感的場合, 都可以優先考慮使用 Parallel Scavenge + Parallel Old 收集器組合. 
 

- CMS 收集器 
CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標的收集器. 目前很大一部分的 Java 應用都集中在網際網路或 B/S 系統的服務端上, 這類應用尤其重視服務的回應速度, 希望系統停頓時間越短以給用戶帶來較好的體驗. CMS 收集器非常符合這類應用需求. 

從名字就可以知道 CMS 收集器是基於 "標記-清除" 演算法實現的, 它的運作過程相對於前面幾種收集器要複雜一些, 整個過程分為 4 個步驟 : 
* 初始標記 (CMS initial mark)
* 並行標記 (CMS concurrent mark)
* 重新標記 (CMS remark)
* 並行清除 (CMS concurrent sweep)

其中初始標記, 重新標記這兩個步驟仍然需要 "Stop The World" (將使用者線程暫停). 初始標記僅僅是標記一下 GC Roots 能直接關連到的物件, 速度很快 ; 並行標記階段就是進行 GC Roots Tracing 的過程 ; 而重新標記則是為了修正並行標記期間, 因使用者程式繼續運作導致標記產生變動那一部分物件的標記紀錄, 這個階段的停頓時間一般會比初始標記稍長一些, 但遠比並行標記的時間短. 

由於整個過程中耗時最長的並行標記與並行清除中, 收集器執行序都可以與用戶執行序一起工作, 所以整體來說 CMS 收集器的記憶體回收過程是與使用者執行序一起並行的執行 : 
 

CMS是一款優秀的收集器 : 並行收集, 低停頓. 但是 CMS 還遠不到完美的程度, 它有以下三個顯著缺點 : 
* CMS 收集器對 CPU 資源非常敏感 
其實面向並行設計的程式都對 CPU 資源比較敏感. 在並行階段, 它雖然不會導致用戶執行緒停頓, 但是會因為占用部分執行緒 (或者說 CPU 資源) 而導致應用程式變慢, 總傳輸量會降低. CMS 預設啟動的回收執行緒是 (CPU 數量 + 3)/4, 也就是 CPU 在 4 個以上時, 並行回收時垃圾收集執行緒最多占用不超過 25% 的 CPU 資源.

* CMS 收集器無法處理浮點垃圾 (Floating Garbage) 
CMS 收集器無法處理浮點垃圾, 可能出現 "Concurrent Mode Failure" 失敗而導致另一次 Full GC 的產生. 由於 CMS 並行清理階段用戶執行緒還在執行著, 伴隨程式的執行自然還會有新的垃圾不斷產生, 這一部分垃圾出現在標記過程之後, CMS 無法在本次收集中處理掉, 只好等待下一次 GC 時再將其清理掉.

* 演算法會產生大量空間碎片 
CMS 是一款基於 "標記-清除" 演算法實現的收集器, 這意味著收集結束時會產生大量空間碎片. 空間碎片過多會給大物件分配帶來麻煩. 往往會出現老年代還有很大的空間剩餘, 但是無法找到足夠大的連續空間來分配當前對象, 不得不提前觸發一次 Full GC. 為了解決這個問題, CMS 收集器提供一個 -XX:+UserCMSCompactAtFullCollection 開關參數, 用於在完成 Full GC 服務之後額外免費送一個磁碟重組過程, 記憶體整理的過程是無法並行的, 空間碎片的問題沒有了, 但是停頓時間卻變長了. JVM 設計者還提供另一個參數 -XX:CMSFullGCsBeforeCompaction, 這個參數用於設置在執行多少次不壓縮的 Full GC 後, 跟著來一次帶壓縮的.

- G1 收集器 
G1 (Garbage First) 收集器是當前收集器技術發展的前端技術, 在 JDK 1.6_Update 14 中提供了 Early Access 版本的 G1 收集器以供試用. 在將來的 JDK 1.7 正式發布時, G1 收集器很可能會有一個成熟的商用版本可以使用. G1 收集器與前面 CMS 收集器相比有兩個顯著改進 : 一是 G1 收集器是基於 "標記-整理" 演算法實現, 也就是說不會產生空間碎片, 這對於長時間執行的應用程式非常重要 ; 二是它可以非常精確地控制停頓, 既能讓使用者明確指定在一個長度為 M 毫秒時間片段內, 消耗在垃圾收集上的時間不得超過 N 毫秒. 

G1 會將整個 Java Heap (包括新生代, 老年代) 劃分為多個大小固定的獨立區域 (Region), 並且跟蹤這些區域裡面的垃圾堆積程度, 在後台維護一個優先列表, 每次根據允許的收集時間, 優先回收垃圾最多的區域 (這就是 First 名稱的由來). 區域劃分及有優先順序的區域回收保證了 G1 收集器在有限時間內可以獲得最高的收集效率 

垃圾收集器參數總結 : 
JDK 1.6 中的各種垃圾收集器到此已經介紹完畢, 下表整理 JVM 可使用參數提供後續研究使用 : 
 
(
 

補充說明 : 
Tomcat查看GC信息

2012年6月28日 星期四

[ JVM 應用 ] 垃圾收集器與記憶體分配策略 - Part1



前言 : 
說起 GC (Garbage Collection), 大多數會以為這項技術是因為 Java 而存在. 事實上 GC 的歷史遠比 Java 久遠, 1960 年誕生於 MIT 的 Lisp 是第一門使用記憶體動態分配與 GC 技術的語言. 而在使用 GC 需要思考的是 : 
* 那些記憶體需要回收
* 什麼時候回收
* 如何回收

經過半世紀的演進, GC 技術已經相當成熟. 至於為什麼我們需要去了解 GC? 因為當需要排查各種記憶體溢出, 洩漏問題, 當 GC 成為系統達到更高運行量的瓶頸時, 我們就需要對這些 "自動化" 的技術實施必要的監控與調整. (相對於 C/C++, 記憶體回收時機都是在Compile 前就決定.

物件已死 : 
Heap 存放著 Java 的物件實例, GC 在對 Heap 進行記憶體回收前第一件事就是要確定那些物件還 "存活" 哪些已經 "死去" (既不可能再被任何途徑使用的物件.), 底下我們要來介紹一些常見的演算法, 說明如何進行這樣的工作. 

- 參考計數演算法 
很多教科書判斷物件是否存活的演算法是這樣 : 給物件中增加一個參考計數器, 每當有一個物件參考它, 計數器就加一 ; 當參考失效, 計數器就減一 ; 任何時刻計數器為 0 的物件就是不可能再被使用的. 客觀地說參考計數演算法 (Reference Counting)的實現簡單, 判斷效率也高. 但是 Java 語言沒有選用這個演算法來管理記憶體, 其中最主要的原因是它很難解決物件之間的互動迴圈參考的問題. 

舉個簡單的例子, 請看下面程式碼 : 對象 objA 和 objB 都有字段 instance, 代碼 objA.instance=objB 和 objB.instance=objA, 除此之外這兩個對象再無 
任何參考, 實際上這兩個物件已經不可能再被訪問, 但是因為他們互相參考著對方, 導致它們的參考計數都不為零, 於是參考計數演算法無法通知 GC 回收它們 : 
  1. package ch03;  
  2.   
  3. public class Ex3_1 {  
  4.     protected String name = "";  
  5.     public Object instance = null;  
  6.       
  7.     public Ex3_1(String name)  
  8.     {  
  9.         this.name = name;  
  10.     }  
  11.     @Override  
  12.     public void finalize() throws Throwable  
  13.     {  
  14.         System.out.printf("\t[Info] %s being GC!\n", name);  
  15.     }  
  16.   
  17.     /** 
  18.      * Goal : 
  19.      *  main() 方法執行後, objA 和 objB 會被會被 GC? 
  20.      * @param args 
  21.      */  
  22.     public static void main(String[] args) throws Exception{  
  23.         Ex3_1 objA = new Ex3_1("objA");  
  24.         Ex3_1 objB = new Ex3_1("objB");  
  25.   
  26.         objA.instance = objB;  
  27.         objB.instance = objA;  
  28.           
  29.         objA = null; objB = null;  
  30.         System.gc();  
  31.         // 等  GC 收集  objA, objB.  
  32.         Thread.sleep(1000);  
  33.         System.out.printf("\t[Info] Byebye!\n");  
  34.     }  
  35. }  
但從執行結果知道在主程式離開之前, 它們就已經被回收, 說明 JVM 並不是使用參考計數演算法來判斷物件是否存活 : 
[Info] objA being GC!
[Info] objB being GC!
[Info] Byebye!

- 根搜尋演算法 
在主流的商業程式語言中 (Java, C# etc) 都是使用 根搜尋演算法 (GC Roots Tracing) 判段物件是否存活. 這個演算法的基本思路就是透過一系列的名為 "GC Roots" 的物件作為起始點, 從這個節點開始向下搜尋, 搜尋所走過的路徑稱為參考鏈 (Reference Chain). 當一個物件到 GC Roots 沒有任何參考鍊相連 (用圖解的話就是從 GC 到這個物件沒有路徑可以到達)時, 則說明此物件無法再被參考或使用. 在 Java 語言中, 可以做為 GC Roots 的對象包括 : 
* 虛擬器堆疊 (堆疊 Frame 中的本地變數表) 中參考的物件.
* 方法區中的類別靜態屬性參考物件
* 方法區中的常數參考的物件
* 本地方法堆疊中 JNI (一般說的 Native 方法) 的參考物件.

再談參考 : 
無論是透過 參考計數演算法 判斷物件的參考數, 還是透過 根搜尋演算法 判斷物件的參考鍊是否可達, 判斷物件手否存活都與 "參考" 有關. 在 JDK 1.2 之前, Java 中的參考定義是 : 如果 reference 類型資料中存儲的數值代表是一塊記憶體的起始位址, 就稱這塊記憶體代表一個參考. 一個物件在這種定義下只有被參考或者沒有參考兩種狀態, 對於如何描述一些較細緻的參考行為並不足以應付後續的應用. 我們希望能描述這麼一類的物件 : 當記憶體空間還足夠時, 能保留在記憶體中 ; 如果記憶體在 GC 後還是不敷使用, 則可以拋棄這些物件. 在 JDK 1.2 之後 Java 對參考的概念進行了延伸, 將參考分為 Strong Reference, Soft Reference, Weak Reference 與 Phantom Reference 四種 : 
* Strong Reference 
Strong Reference 就是指在程式碼中普遍存在, 類似 Object obj = new Object() 這類的參考, 只要 Strong Reference 存在, GC 永遠不會回收該物件.

* Soft Reference 
Soft Reference 用來描述一些還有用, 但是非必須的物件. 對於 Soft Reference 的物件, 在系統即將發生記憶體溢出之前, 將會把這些物件列入回收清單中並進行第二次回收. 如果這次回收過後還是沒有足夠記憶體, 才會跳出記憶體異常. 在 JDK 1.2 後提供了 SoftReference 類別來實現 Soft Reference.

* Weak Reference 
Weak Reference 也是用來描述非必須物件, 但是它的強度比 Soft Reference 更弱一點, 被弱參考關聯的物件只能生存到下一次的 GC 前. 當 GC 工作時, 無論當前記憶體是否足夠, 都會回收掉只被 Weak Reference 的物件. 在 JDK 1.2 後, 提供了 WeakReference 類來實現.

* Phantom Reference 
Phantom Reference 也稱為 幽靈參考或者幻影參考, 它是最弱的一種參考關係. 一個物件是否有 Phantom Reference 完全不會對其生存時間構成影響, 也無法透過它來取的一個物件實例. 為物件設置 Phantom Reference 的唯一目的就是希望能在這個物件被 GC 時收到一個系統通知. 在 JDK 1.2 提供 PhantomRefernce 類別來實現.

生存還是死亡 : 
在根搜尋演算法中不可達的物件, 也並非馬上會被 GC, 這時候它們處於 "緩刑" 階段, 要宣告一個物件被 GC, 至少要經歷兩次標記過程 : 如果物件在進行根搜尋後沒有發現與 GC Roots 相連接的參考鍊, 那它將被第一次標記並進行一次篩選, 篩選的條件是此物件是否有必要執行 finalize() 方法. 當物件沒有覆蓋 finalize() 方法, 或者 finalize() 已經被 JVM 呼叫過, JVM 將這兩種期況視為 "沒有必要執行". 

如果這個物件被判定有必要執行 finalize() 方法, 那麼這個物件將會被放置在一個名為 F-Queue 的佇列中, 並在稍後由一條由 JVM 自動建立, 低優先順序的 Finalizer 執行序去執行. finalize() 方法是物件逃脫被 GC 的最後一次機會, 稍後 GC 將對 F-Queue 中的物件進行第二次小規模的標記, 如果物件要在 finalize() 中成功擺脫被 GC - 只要重新與參考鏈上的任何一個物件建立關聯即可, 那在第二次標記時就會將它移除 "即將回收" 的集合 ; 如果物件這時候還沒逃脫, 那它真的就會被 GC 了, 下面代碼我們可以看到一個物件的 finalize() 被執行, 但是它仍然沒被 GC : 
  1. package ch03;  
  2.   
  3. public class Ex3_2 {  
  4.     public static Ex3_2 SAVE_HOOK = null;  
  5.       
  6.     public void isAlive(){System.out.printf("\t[Info] Yes, I am still alive :)\n");}  
  7.   
  8.     @Override  
  9.     public void finalize() throws Throwable{  
  10.         super.finalize();  
  11.         System.out.printf("\t[Info] finalize method executed!\n");  
  12.         Ex3_2.SAVE_HOOK = this;  
  13.     }  
  14.       
  15.     /** 
  16.      * Goal : 
  17.      *  1. Object can be saved from GC 
  18.      *  2. The chance only once. Because the finalize() will at most being called once. 
  19.      * @param args 
  20.      */  
  21.     public static void main(String[] args) throws Exception{  
  22.         SAVE_HOOK = new Ex3_2();  
  23.         test(); // 第一次 finalize() 執行  
  24.         test(); // finalize 只會被執行一次!  
  25.     }  
  26.       
  27.     public static void test() throws Exception  
  28.     {  
  29.         SAVE_HOOK = null;  
  30.         System.gc();  
  31.         // Finalizer has low priority, sleep 0.5 sec to wait.  
  32.         Thread.sleep(500);  
  33.         if(SAVE_HOOK!=null)  
  34.         {  
  35.             SAVE_HOOK.isAlive();  
  36.         }  
  37.         else  
  38.         {  
  39.             System.out.printf("\t[Info] No, I am dead :(\n");  
  40.         }  
  41.     }  
  42. }  
執行結果如下 : 
[Info] finalize method executed!
[Info] Yes, I am still alive :)
[Info] No, I am dead :(

可以看到SAVE_HOOK 的對象 finalize() 被 GC 執行並且成功逃脫 GC, 但是因為 finalize() 只能被執行一次, 故第二次就被 GC 了. 

回收方法區 : 
很多人認為方法區 (或者在 HotSpot JVM 中的 Permanent Generation) 是不會進行 GC 的, JVM 規範中確實說過可以不要求 JVM 在方法區中進行 GC, 而且在方法區進行 GC 的效率一般比較低 : 在 Heap 中, 尤其是新生代中, 常規應用進行一次 GC 一般可以回收 70%~95% 空間, 而永久代效率遠低於此. 

永久代的 GC 主要回收兩部分內容 : 廢棄常數和無用的類別. 回收廢棄常數與回收 Java Heap 中的物件非常類似. 以常數池中字面變數回收為例, 假如一個字串 "abc" 已經進入常數池, 但是當前系統沒有任何一個 String 物件內容是 "abc" 的, 換句話說就是沒有任何 String 物件參考常數池中的 "abc" 常數, 也沒有其他地方參考了這個字面參數, 如果發生 GC 且有必要, 這個 "abc" 常數就會被系統請出 常數池. 而判斷一個常數是否為 "廢棄常數" 比較簡單, 而要判斷一個類別是否為 "無用的類別" 的條件相對複雜許多. 類別需要同時滿足以下 3 個條件才能算是 "無用的類別" : 
* 該類別所有的實例都已經被回收, 也就是 Java Heap 中不存在該類別的任何實例.
* 載入該類別的 ClassLoader 已經被回收
* 該類別對應的 java.lang.Class 物件已經沒有在任何地方被參考, 無法在任何地方透過反射訪問該類別方法.

JVM 可以對滿足上述 3 個條件的類別進行回收, 這裡僅是 "可以", 而不是和物件一樣不使用就必然會回收. 是否對類別進行回收, HotSpot JVM 提供了 -Xnoclassc 參數進行控制, 還可以使用 -verbose:class 及 -XX:+TraceClassLoading-XX:+TraceClassUnLoading 查看類別的載入與卸載訊息. 在大量使用反射, 動態代理, CGLib 等 bytecode 框架的場景以及動態產生 class 的 JSP, OSGi 這類頻繁自訂 ClassLoader 的場景都需要 JVM 具備類別卸載的功能, 以保證永久代不會溢出. 

補充說明 : 
[ Gossip in Java(2) ] 反射 : 檢視類別 (簡介 Class)
[ JVM 應用 ] 垃圾收集器與記憶體分配策略 - Part2

[ Python 常見問題 ] How to shift a datetime object by 12 hours in python

Source From   Here   Question   Datetime   objects hurt my head for some reason. I am writing to figure out   how to shift a date time obje...