一、前言
前段時間大致看了一下《深入理解Java虛擬機》這本書,對相關的基礎知識有了一定的了解,准備寫一寫JVM
的系列博客,這是第二篇。這篇博客就來談一談JVM
中使用到的垃圾回收算法。
二、正文
2.1 什么是垃圾回收
在正式介紹垃圾回收算法前,先來說說什么是垃圾回收。這里所說的垃圾主要指的是已經不會再繼續使用的對象,當然也有可能是其他,比如不再使用的類以及常量,但主要還是指對象,所以以下算法將介紹對象的回收。所以垃圾回收的含義就是:將內存中已經不會被使用的對象(或類和常量)清除,釋放內存空間。
JVM
的內存模型分為五個部分,其中堆內存的唯一目的就是存放對象,對象也基本上都是存放在堆內存中。堆中,為了方便進行垃圾回收,一般會將內存分為兩個部分:
- 新生代:用來存放生命周期短的對象。由於這一塊內存中的對象存活時間較短,所以頻繁發生垃圾回收,而且每次回收一般都能釋放大量空間;
- 老年代:用來存放生命周期長的對象。新生代中存活了較長時間的對象會被遷移到這里(當然,對象進入老年代不僅僅只有這一個方法),所以這里存放的對象生命周期一般較長,所以這一塊區域發生垃圾回收的頻率較低,釋放的空間也較少;
下面正式開始討論JVM
中的垃圾回收算法。
2.2 如何識別垃圾
進行垃圾回收的第一步就是找到垃圾(我們這里主要以對象為例),也就是無法被使用的對象。對象在什么情況下無法被使用?很簡單,沒有引用指向這個對象,我們自然無法使用它,比如看下面這段代碼:
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
a = null;
}
上面的代碼中,我創建了一個對象,並使用變量a
指向這個對象,但是在這之后,我又將null
賦給了a
,這會出現什么情況?不難發現,我們已經無法使用這個對象了,它已經丟失了,因為我們已經無法通過任何變量去調用這個對象,但是它依然在內存中。此時,這個對象占用着內存就是白白浪費資源,我們希望它被清除。所以,我們可以想到,當一個對象沒有引用指向它時,就可以認為他是一個垃圾對象了。
(1)引用計數法
引用計數法就是通過引用來識別無用對象。我們記錄每一個對象的引用個數,若有新的變量引用一個對象時,這個對象的引用個數加1;若一個引用失效時,引用的個數減1,而引用個數為0的對象,即可作為垃圾被回收。這里要注意,若這些垃圾對象的成員變量引用了其他對象,則當垃圾對象被釋放時,它的這個引用自然就失效了。
這個算法實現簡單,效率也高,但是,它並沒有被用在主流的Java
虛擬機中,因為它有一個很大的缺陷——很難解決循環引用的問題。什么是循環引用,看下面一段代碼:
public class Main {
private Object obj;
public static void main(String[] args) {
Main m1 = new Main();
Main m2 = new Main();
// 循環引用
m1.obj = m2;
m2.obj = m1;
m1 = null;
m2 = null;
}
}
上面這段代碼中,創建了兩個對象m1
和m2
,它們都有一個屬性obj
。而m1
的obj
指向了m2
,而m2
的obj
指向了m1
。多個引用形成一個環,這就是循環引用。這對於使用引用計數算法的垃圾回收器來說有一個問題,即上面的代碼最后,m1
和m2
都置為了空,它們指向的兩個對象已經無法再使用了,但是由於這兩個對象相互引用,導致它們的引用計數並不為0
,所以垃圾回收器不會將它們判別為無用對象。正是因為這個問題的存在,Java
中的垃圾回收器基本上不使用這個算法。
(2)可達性分析法
可達性分析法是Java
垃圾回收中判別無用對象的主要方法。這個方法的步驟是,從根節點對象出發,使用DFS
或BFS
算法,沿着引用遞歸遍歷,而無法被遍歷到的對象,就是無法再被使用的對象,可以被垃圾回收器回收。所謂的根節點,就是我們能夠直接使用的引用類型變量,如:
- 方法中的參數或局部變量;
- 類的靜態成員或非靜態成員;
- 代碼中的常量;
這種方法的效率相對於引用計數來說相對復雜,而且效率較低,但是解決了循環引用的問題,是Java
垃圾回收中主要使用的方法。
2.3 如何釋放垃圾
釋放垃圾指的就是清除無用對象,釋放它們所占的內存空間,方便繼續使用。這里主要介紹三種方法:
- 標記—清除算法;
- 復制算法;
- 標記—整理算法;
這三種算法根據具體情況的不同,搭配使用,才能發揮最好的效果。下面就來一一介紹。
(1)標記—清除算法(Mark-Sweep)
標記—清除是以上上面三種算法中最基礎的一種,為什么說它是最基礎的,因為它的原理非常簡單。故名思意,這個算法分為兩個步驟:(1)標記;(2)清除。
- 標記:標記指的就是我們上面所說的可達性分析,采用之前所說的可達性分析算法遍歷對象,所有不可達的對象將被標記為垃圾,等待回收;
- 清除:這一步很簡單,直接釋放垃圾對象所占內存空間;
這個算法有兩個的問題:
- 效率較低,標記和清除這兩個步驟的效率都比較低,清除的效率低是因為需要掃描整個內存空間,逐個釋放對象所占內存;
- 使用這個算法清除垃圾后,將會造成很多內存碎片,所以可能出現剩余內存較多,但是沒有較大的連續空間,導致大對象無法被分配空間,而再次觸發垃圾回收;
我們通過兩張對比圖來看看這個算法的效果。通過下面這張圖我們可以看到,在垃圾回收后造成了很多的內存碎片。
(2)復制算法(Copying)
為了解決效率較低以及產生內存碎片的問題,有人提出了一個新的算法——復制算法。這個算法的原理是:將內存分為兩個相等大小的區域,一塊存放對象,一塊保留。當存放對象的那塊區域無法再分配空間時,將所有仍然存活的對象復制到保留的那塊區域中,然后直接釋放當前正在使用區域的全部內存。這樣一來,仍然存活的對象被放進保留區,而垃圾對象也被釋放了。同時,之前被使用的空間被清空后,成了新的保留區,而之前的保留區成了被使用的空間,就這樣不斷循環使用兩個空間。
我們之前提過,堆內存被分為新生代和老年代。在新生代中,每次垃圾回收都可以釋放大量的對象,只有少部分存活,所以只有少部分對象要被復制到保留區中,這也意味着復制並不會太耗時。除此之外,直接釋放被使用的空間的全部內存,比一段一段釋放的效率也要高很多。同時,對象被復制到另外一個區域時,會被整齊地擺放,所以不會出現內存碎片,所以能夠更簡單地分配空間。所以,復制算法的效率要遠遠高於標記—清除算法。以下是一張復制算法的演示圖:
但是,這里存在一個問題,復制算法將內存區域划分為相等的兩部分,這也意味着每次都有一半的空間無法被使用,這未免也太浪費了。所以,對於空間的划分,需要做出一些改進。IBM
公司的研究表明,98%
的對象存活時間都非常的短暫,所以,完全沒有必要保留一半的空間供復制使用。在實際實現中,會將空間划分為三塊區域,一塊較大的Eden
空間,以及兩塊較小的Survivor
空間。在為新對象分配空間時,首先會將其分配到Eden
空間中,若Eden
空間無法再分配空間時,將會觸發垃圾回收,此時,會將Eden
空間中的存活對象復制到其中一塊Survivor
空間中,然后清空Eden
空間。當Eden
空間再一次因無法分配空間而觸發垃圾回收時,則會將Eden
空間中的存活對象,以及上一次被復制進Survivor
空間中的存活對象,都復制到另一塊Survivor
空間中,然后將Eden
和上一塊Survivor
清空。也就是說,交替地使用兩塊Survivor
空間,來存放垃圾回收中任然存活的對象。而在具體實現中,這三個空間的比例一搬是8:1:1
,即是說只有10%
的空間無法被使用。
可以看出,這個算法在大部分對象的生命周期都短時,效率會非常高,但是若大部分對象的生命周期都很長,將不再適用,所以這個算法一般只被用在新生代中。這里我們不得不考慮一個問題,當我們使用了上面說的將內存划分為三塊的這種方式時,可能會出現一個問題:如果在某次垃圾回收過后,仍然有大量的對象存活,此時一個Survivor
空間不夠存放這些對象怎么辦?這時候就需要有另一個空間來做擔保了,當這種情況發生時,會將這些對象放入另一個空間中,那個空間就叫做擔保空間。就像我們去銀行貸款,需要有一個擔保人,當貸款人不能償還時,由擔保人代為償還。以上算法是用在新生代中,而所謂的擔保空間,實際上就是老年代。老年代為這個算法提供了擔保,但是在大部分情況下,Survivor
都是能夠滿足需求的。
(3)標記—整理(Mark-Compact)
由於老年代中的對象一般存活時間都比較長,所以並不適合在老年代使用上面的復制算法進行垃圾回收。而有人根據老年代的特點,提出了標記—整理算法,注意看清楚,這里是整理,而不是第一種算法中的清除。這個算法也分為標記和整理兩個步驟,標記這個步驟和第一個算法是一樣的,關鍵是整理步驟。所謂的整理,就是將內存中還存活的對象向一邊移動,直至這些對象相互靠攏,整齊排列,然后直接清除不屬於這一部分的全部內存。標記—整理的好處是解決內存碎片的問題。以下是這個算法的演示圖:
(4)分代收集算法
分代收集算法並不是什么新思想,而是對上面三種算法的綜合使用。前面也提過,為方便垃圾回收,一般將堆內存分為新生代和老年代兩個部分。
- 對於新生代而言,這一塊區域中的對象存活時間短,每一次垃圾回收都能回收大部分內存,所以適合使用復制算法,同時以老年代作為這個算法的擔保空間;
- 對於老年代而言,每次垃圾回收只能釋放小部分空間,若使用復制算法,每次將需要做大量復制,而且此時
Survivor
需要較大的空間,所以不適合使用復制算法,因此在老年代中,一般使用標記—清除或者標記—整理算法;
三、總結
上面對JVM
中的垃圾回收算法做了一個比較詳細的介紹,相信看完這一篇博客會對這部分內容有更深的理解。但是,歸根到底,上面的內容只是理論,接下來我將寫一篇博客,來講講JVM
具體如何分配和釋放對象,作為JVM
系列博客的第三篇。
四、參考
- 《深入理解Java虛擬機》