垃圾回收(GC)是JVM的一大殺器,它使程式設計師可以更高效地專注於程式的開發設計,而不用過多地考慮物件的建立銷燬等操作。但是這並不是說程式設計師不需要了解GC。GC只是Java程式設計中一項自動化工具,任何一個工具都有它適用的範圍,當超出它的範圍的時候,可能它將不是那麼自動,而是需要人工去了解與適應地適用。
擁有一定工作年限的程式設計師,在工作期間肯定會經常碰到像記憶體溢位、記憶體洩露、高併發的場景。這時候在應對這些問題或場景時,如果對GC不瞭解,很可能會成為個人的發展瓶頸。
接下來的兩文將詳細學習下JVM中垃圾回收(GC)的各個知識要點。本文先從GC的演算法開始先了解,鋪墊好基礎,下一篇再詳細講JVM具體的GC實現。
GC物件搜尋演算法
垃圾回收,第一件事就是要搞清楚哪些東西是垃圾,而後才能對這些垃圾進行回收。
那麼有什麼辦法識別物件是否為無用的垃圾呢?狹義地,怎麼判斷物件是否沒被引用呢?
通常有以下兩種演算法去識別判斷
-
引用計數演算法
這個演算法非常簡單。給物件一個計數器,每當這個物件被引用了,計數器值加一;引用失效,則減一。但這個物件計數值為0的時候,證明是無用物件,可以被GC程式回收掉。這種演算法比較廣泛應用在一些指令碼語言上,如FLASH、PYTHON等。
但是引用計數演算法無法解決物件間相互引用的問題。當a物件引用了b物件,b物件也引用了a物件,這樣a、b兩個物件的計數器值都不會為0,即使這兩個物件都被其他物件所引用,最終導致這些物件一直無法被回收。這種情況往往會出現在比較複雜的程式語言中。 -
可達性分析演算法
可達性分析演算法(GC roots演算法),廣泛應用於主流的商用語言。設定一個根節點,從圖論角度來看,只要從該節點可達一個物件,證明這個物件是存活的(被引用)。
通常地,GC會包含以下區域的物件:
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件;
- 方法區中類靜態屬性引用的物件;
- 方法區中常量引用的物件;
- 本地方法棧中JNI(即一般說的Native方法)引用的物件;
垃圾回收演算法
瞭解完垃圾是怎麼找出來後,接下來看看它們是怎麼被清除的。以下介紹幾種清除的演算法。
標記-清除演算法(Mark-Sweep)
標記-清除,顧名思義,先標記垃圾,再清除。它是GC最基礎的演算法,後續很多演算法都是基於它上面去改進的。
標記的過程在上面搜尋GC物件已經介紹過了。被標記的物件,在統一GC的時候會把標記的物件清除掉。這個演算法比較簡單,不做過多贅述。
這個演算法有一個很明顯的缺點,就是在垃圾回收後會產生大量不連續的碎片空間,導致程式要申請較大的物件時常無法找到合適的記憶體空間,迫使再次GC。
複製演算法
複製演算法的存在,正是為了解決記憶體碎片問題。並且這個演算法也是分代演算法的基礎。
將記憶體分為大小相等的兩塊,每次程式只使用其中一塊,當GC發生的時候,把存活的物件複製到另外一塊記憶體中,整齊的排列,然後清空原來的那塊記憶體。
可以看到,這種演算法有點新生代轉移到老年代的感覺。
缺點:
- 把記憶體可使用的空間減少了一半,造成空間的浪費。
- 物件存活數量較多的時候,複製效能比較差
這種缺點,在老年代中,物件存活率比較高的場景下是非常場景間。
標記-整理演算法(Mark-Compact)
針對複製演算法的兩個缺點,在老年代一般會用這種標記-整理演算法。
把存活的物件移到記憶體的一段,然後把剩餘的空間全部清空掉。
分代收集演算法
分代演算法並不是一個特定的演算法,也沒有什麼新的內容。而是把記憶體分成多個區域,一般為新生代、老年代等。然後根據不同區域不同的特點,用不同回收演算法去回收垃圾。
例如新生代,物件存活率低,比較適用複製演算法。老年代存活率高,比較適用Mark-Compact演算法。
目前幾乎所有的商業虛擬機器都是採用分代收集的。具體不同的收集器在下一文再詳細說明。
更多技術文章、精彩乾貨,請關注
部落格:zackku.com
微信公眾號:Zack說碼