通常來說,要寫Java代碼,你基本上都沒必要聽說垃圾回收這個概念的。這不,對於已經寫了多年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:+UseParallelGC java 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激活該收集器。