提到JVM垃圾回收,總覺得離我們程序員有一定的距離。在JAVA中,那是系統自己干的事,我們關心那個干嘛?也就是說我們為什么要學習這個東西,大家開開心心地敲代碼不好嗎?
還真的不好,一方面我覺得我們可以學習下JAVA語言設計上的一些思想,另一方面,在我們以后從事一些較為高級一點的開發,尤其是性能調優之類的,知道這些基礎知識就顯得很必要了。我打算從以下幾個方面開始進行簡單地說明。
GC如何知道哪些對象是垃圾對象?
GC不可能隨便指派說哪個對象是垃圾,要有一定的依據。常用的標記垃圾的算法有兩個:
引用計數算法
引用計數算法,就是每個對象有一個引用計數器,當該對象被引用的時候計數器加1,當引用失效的時候,計數器減1。
那么這么做有什么缺點嗎?
那就是當兩個對象相互引用的時候,這兩個對象都會無法釋放。
根搜索算法
從根對象開始,所有能被觸及的對象都可以認為是“存活的”對象,換句話說,就是“仍然使用的”對象。不能被觸及的對象,就會被認為是垃圾,需要回收。

那么什么對象可作為GC Roots呢?
- 虛擬機棧(棧幀中的本地變量表)中引用的對象;
- 方法區中類靜態屬性的引用;
- 方法區中常量引用的對象;
- 本地方法棧中JNI(Native方法)引用的對象。
什么是虛擬機棧?
虛擬機棧為Java方法服務。說的簡單點,就是我們平時所寫的Java方法,沒調用一個Java方法,就是入棧的過程,退出方法就是出棧的過程。每個Java方法對應于虛擬機棧中的一個棧幀。
什么是本地方法棧?
本地方法棧為native方法服務。
方法區是什么呢?
方法區與Java堆一樣,是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據。可以看做堆的一個邏輯部分。
常用的垃圾回收算法有哪些?
看下圖,整幢大樓燈火通明,其中不排除一些辦公室沒人但還是燈亮著的情況,為了節約資源,我們需要關掉那些辦公室沒人的燈。那么怎么辦呢?

標記——清除算法
我從頂樓開始到一樓,一塊一塊辦公區域看,對沒人的區域進行關燈。將整個大樓看成內存,燈亮著的區域表示有對象存在,燈滅著的表示空閑區域。我們一塊一塊區域檢查的這個過程就是標記的過程,關燈的操作就是清除的過程。這就是標記——清除算法。

弊端:
- 那就是費時費力,效率太低。
- 不連續,不美觀(內存碎片嚴重)。
復制回收算法
老板說了,浪費太嚴重了,到了晚上,我們對需要加班的同事進行統一安排。假設大樓共10層,只能使用15層或者610層(畢竟晚上加班的人不多)。比如現在使用的就是15樓,到了晚上要用燈了,需要加班的同事自己去610樓找位置,保安一聽樂了,再也不用一塊一塊區域關燈了,有需要的人都去610樓了,剩下的即便是燈亮著的辦公區域那也是沒人,讓我分別去15樓拉個總閘先(回收的過程)。

可以看到,在任意時刻只用到了內存的一半。
弊端:
- 有需要的同事搬到6~10樓的過程,太麻煩。特別是需要加班的同事比較多的時候。(需要對有用的對象進行復制)。
- 整棟大樓只能用一半,哎(內存使用率降低)。
優點:
從外面看,我知道哪些地方有人,哪些地方沒人,方便了管理(內存無碎片)。
標記——整理算法
下班后,保安大哥將空的辦公區域依次統計出來(標記的過程),需要加班的同事按照統計結果,依次搬到空閑的辦公區域。保安大哥知道,我只需要找到最后一個有人的區域,那么這塊區域之后肯定不會有人了,不用挨個檢查了,去拉后面的閘。

優點:
- 內存無碎片。
- 同時避免了當有用對象比較多的時候,復制回收算法的麻煩。
分代回收算法

新生代:
剛創建的對象都在新生代,新生代采用復制回收算法。新生代分為三個區,一個Eden區,一般兩個Survivor區。大部分對象在Eden區生成,當Eden區域滿時,將還存活的對象復制到其中一個Survivor區域,當這個Survivor區域滿時,將其中還存活的對象復制到第二個Survivor區域。那么當第二個Survivor區域滿時該怎么辦呢?那就是將第二個Survivor區域中由第一個Survivor區域復制過來的對象,復制到“老年代”中。
這個過程是有點繞,但是可以想象成面試過程中層層選拔的過程,能力越強的可以想象成生命周期越長的對象。
老年代:
這個區域中的對象都是在新生代中經歷了層層回收后仍然存活的對象,這個區域采用標記整理的算法進行垃圾回收。
持久代:
持久代中用于存放一些靜態文件,static常亮,常量池等。這塊區域對垃圾回收沒有顯著影響。
什么時候會進行垃圾回收?
GC有兩種類型:Minor GC和Full GC。
Minor GC
當新對象生成,并且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清理非存活對象。
Full GC
對整個堆進行整理,所以比Minor要慢,所以盡可能地減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對于Full GC的調節。有如下原因可能導致Full GC:
- 老年代被寫滿;
- 持久代被寫滿;
- System.gc()被顯式調用。
參考資料:《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版)》