標籤:cpu 標記 並發 大小 9.png log height nbsp 記憶體
通常來說,要寫Java代碼,你基本上都沒必要聽說記憶體回收這個概念的。這不,對於已經寫了5年多Java代碼的我來說,我還沒有哪次經曆說是需要使用記憶體回收方面的知識來解決問題的。但是,我依然督促自己花了幾天時間系統性地(也比較淺顯地)學習了Java記憶體回收機制。我認為學習Java記憶體回收機制至少可以得到以下幾方面的好處:
- 對於系統調優有直接協助
- 增加和同行聊天或者下一份工作面試時的談資
- 在追求技術卓越上更進一步
(一)Java堆記憶體的分代管理
Java記憶體回收是需要消耗CPU和記憶體資源的,其速度隨著記憶體的變大而減慢,這將嚴重影響系統的效能。同時,Java系統中存在著這麼一種現象:大多數Java對象都是“短命”的。基於此,Java採用了分代的記憶體管理方式,並在不同的記憶體代中採用不同的記憶體回收演算法,從而達到對記憶體更細粒度的管理,最大限度地減小記憶體回收對系統本身的影響。
由所示,Java的堆空間被分為了三個地區,分別是新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)。新建立出來的對象首先存放在新生代,經過新生代中多次記憶體回收(在Survivor 0和Survivor 1之間來回複製),存活下來的對象將被轉移到老年代。新生代中記憶體回收很頻繁,這樣多數“短命”的對象將得到及時清理;又由於新生代記憶體空間通常不大,回收速度也相對較快。在老年代中,存放著從新生代中經曆了多次記憶體回收後仍然存活的對象,這些對象相對較少,而老年代記憶體一般很大,並不容易塞滿,因此老年代的記憶體回收頻率要遠遠低於新生代,從而減少了對系統效能的影響。永久代中主要存放Java類本身的資料資訊,當Java類不再被使用時,也會被記憶體回收掉。開發人員們通常無法預測永久代的大小,導致程式經常出現 “java.lang.OutOfMemoryError: Permgen space”錯誤,因此在Java 8中,使用jvm進程原生記憶體空間的Metaspace代替了永久代。在預設情況下,Metaspace將使用jvm進程所有可用的記憶體。 在新生代進行的GC叫做minor GC,在老年代進行的GC都叫major GC,Full GC同時作用於新生代和老年代。在記憶體回收過程中經常涉及到對對象的挪動(比如上文提到的對象在Survivor 0和Survivor 1之間的複製),進而導致需要對對象引用進行更新。為了保證引用更新的正確性,Java將暫停所有其他的線程,這種情況被稱為“Stop-The-World”,導致系統全域停頓。Stop-The-World對系統效能存在影響,因此記憶體回收的一個原則是盡量減少“Stop-The-World”的時間。 展示了不同垃圾收集器的Stop-The-World情況,可以看出Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情況;而即便是最新的G1收集器也不例外。
(二)記憶體回收演算法 最早的記憶體回收演算法有引用計數法,但由於其效能不好以及無法回收循環參考對象的問題,工程上並沒有得到使用。當前Java的記憶體回收主要基於標記-清除(Mark-Sweep)演算法,該演算法大致包括兩個步驟:
- 從GC ROOT對象開始標記所有可達對象,GC ROOT包括局部變數、靜態變數及運行中的線程對象等。
- 清除掉未被標記的對象
標記-清除演算法是Java記憶體回收的基本原則,在此基礎上,Java還提供了幾種變種演算法,包括標記-壓縮(Mark-Sweep-Compact)演算法和標記-複製(Mark-Copy)等。
標記清除演算法(Mark Sweep)標記清除演算法的原理即上文中提到的兩個步驟,這種演算法的優點是可以減少Stop-The-World的時間,缺點是會造成記憶體片段,如所示:
標記壓縮演算法(Mark Sweep Compact)
為瞭解決記憶體片段問題,標記壓縮演算法(如所示)在回收記憶體之後會將存活的對象集體壓到記憶體的一端。壓縮過程需要更新對象的引用,如前文所述,這將增加系統Stop-The-World時間。
標記複製演算法(Mark Copy)
標記複製演算法是一種效率相對較高的演算法,因為它不涉及對無用對象的刪除,只需要將標記存活的對象從一個記憶體區拷貝到另一個記憶體區。但是標記複製演算法不適用於存活對象較多的老年代,因為大量的對象拷貝會降低系統效能。Java在新生代中主要採用了標記複製演算法,其中包括從Eden區到Survivor區的複製和兩個Survivor區之間的複製。
(三)垃圾收集器 在Java中主要有4中垃圾收集器,他們各自對於不同的記憶體代採用不同的演算法。Java會根據當前系統的基本配置確定一個預設的垃圾收集器,你可以通過以下命令查看:
java -XX:+PrintCommandLineFlags -version
在筆者的電腦上輸出為:
-XX:InitialHeapSize=268435456 -XX:+PrintCommandLineFlags -XX:+UseParallelGCjava version "1.8.0_45"Java(TM) SE Runtime Environment (build 1.8.0_45-b14)Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
由紅色部分可以看出,預設情況下使用了Parallel收集器,這也是多數Java機器(特別是伺服器)預設的垃圾收集器。
串列收集器(Serial Collector)
顧名思義,串列收集器指採用單線程進行記憶體回收,回收時會導致長時間的Stop-The-World,主要用於單機程式。該收集器在新生代採用複製演算法,在老年代採用標記-壓縮演算法。可以通過-XX:+UseSerialGC命令列選項啟用該收集器。
並行收集器(Parallel Collector)該收集器同樣在新生代採用複製演算法,在老年代採用標記-壓縮演算法,只是使用了多線程的方式進行記憶體回收,從而大大提高了回收效率,但是回收過程中同時需要Stop-The-World。可以通過-XX:+UseParallelGC啟用該收集器。多數情況下,並行收集器是Java的預設收集器。
並發標記清除收集器(Concurrent Mark Sweep Collector,CMS)該收集器在在新生代中採用複製演算法,在老年代採用標記-清除演算法(不是標記-壓縮)。之所以叫“並發”,是因為在回收過程的某些階段,回收線程和使用者線程同時執行,當然不是整個回收過程都可以和使用者線程並行的,該收集器也存在Stop-The-World的時候,只是相比於其他收集器來說Stop-The-World期間較少而已。可以通過-XX:+UseConcMarkSweepGC啟用該收集器。
G1收集器(Garbage First Collector) G1收集器是Java世界最新的收集器,在Java 9中,它將成為預設的垃圾收集器。該收集器採用與上文中提到的收集器不同方式來對待Java對記憶體,如所示。可以通過-XX:+UseG1GC啟用該收集器。
Java記憶體回收學習筆記