一. 垃圾回收的意義
? ? ? 在C++中,對象所占的內存在程序結束運行之前一直被占用,在明確釋放之前不能分配給其它對象;而在Java中,當沒有對象引用指向原先分配給某個對象的內存時,該內存便成為垃圾。JVM的一個系統(tǒng)級線程會自動釋放該內存塊。垃圾回收意味著程序不再需要的對象是"無用信息",這些信息將被丟棄。當一個對象不再被引用的時候,內存回收它占領的空間,以便空間被后來的新對象使用。事實上,除了釋放沒用的對象,垃圾回收也可以清除內存記錄碎片。由于創(chuàng)建對象和垃圾回收器釋放丟棄對象所占的內存空間,內存會出現(xiàn)碎片。碎片是分配給對象的內存塊之間的空閑內存洞。碎片整理將所占用的堆內存移到堆的一端,JVM將整理出的內存分配給新的對象。
垃圾回收能自動釋放內存空間,減輕編程的負擔。這使Java 虛擬機具有一些優(yōu)點。首先,它能使編程效率提高。在沒有垃圾回收機制的時候,可能要花許多時間來解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾回收機制可大大縮短時間。其次是它保護程序的完整性, 垃圾回收是Java語言安全性策略的一個重要部份。
垃圾回收的一個潛在的缺點是它的開銷影響程序性能。Java虛擬機必須追蹤運行程序中有用的對象,而且最終釋放沒用的對象。這一個過程需要花費處理器的時間。其次垃圾回收算法的不完備性,早先采用的某些垃圾回收算法就不能保證100%收集到所有的廢棄內存。當然隨著垃圾回收算法的不斷改進以及軟硬件運行效率的不斷提升,這些問題都可以迎刃而解。
二、垃圾收集的算法分析
? ? ?既然是要進行自動GC,那必然會有相應的策略,而這些策略解決了哪些問題呢,粗略的來說,主要有以下幾點。
1、哪些對象可以被回收。
2、何時回收這些對象。
3、采用什么樣的方式回收。
具體算法:
1、引用計數(shù)法(Reference Counting Collector)
? ? ? 引用計數(shù)法是唯一沒有使用根集的垃圾回收的法,該算法使用引用計數(shù)器來區(qū)分存活對象和不再使用的對象。一般來說,堆中的每個對象對應一個引用計數(shù)器。當每一次創(chuàng)建一個對象并賦給一個變量時,引用計數(shù)器置為1。當對象被賦給任意變量時,引用計數(shù)器每次加1當對象出了作用域后(該對象丟棄不再使用),引用計數(shù)器減1,一旦引用計數(shù)器為0,對象就滿足了垃圾收集的條件。
基于引用計數(shù)器的垃圾收集器運行較快,但是有個致命缺陷:那就是對于循環(huán)引用的對象無法進行回收。
public class Object {
Object field = null;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
Object objectA = new Object();
Object objectB = new Object();//1
objectA.field = objectB;
objectB.field = objectA;//2
//to do something
objectA = null;
objectB = null;//3
}
});
thread.start();
while (true);
}
}
? ? ? 標注了1、2、3三個數(shù)字,當?shù)?個地方的語句執(zhí)行完以后,兩個對象的引用計數(shù)全部為1。當?shù)?個地方的語句執(zhí)行完以后,兩個對象的引用計數(shù)就全部變成了2。當?shù)?個地方的語句執(zhí)行完以后,也就是將二者全部歸為空值以后,二者的引用計數(shù)仍然為1。根據(jù)引用計數(shù)算法的回收規(guī)則,引用計數(shù)沒有歸0的時候是不會被回收的。
根搜索算法
? ? ? 大多數(shù)垃圾回收算法使用了根集(root set)這個概念;所謂根集就是正在執(zhí)行的Java程序可以訪問的引用變量的集合(包括局部變量、參數(shù)、類變量),程序可以使用引用變量訪問對象的屬性和調用對象的方法。垃圾回收首先需要確定從根開始哪些是可達的和哪些是不可達的,從根集可達的對象都是活動對象,它們不能作為垃圾被回收,這也包括從根集間接可達的對象。而根集通過任意路徑不可達的對象符合垃圾收集的條件,應該被回收。下面介紹幾個常用的算法。說到GC roots(GC根),在JAVA語言中,可以當做GC roots的對象有以下幾種:1、虛擬機棧中的引用的對象。2、方法區(qū)中的類靜態(tài)屬性引用的對象。3、方法區(qū)中的常量引用的對象。4、本地方法棧中JNI的引用的對象。
第一和第四種都是指的方法的本地變量表,第二種表達的意思比較清晰,第三種主要指的是聲明為final的常量值
2、tracing算法(Tracing Collector) 標記-清除
根搜索算法,它可以解決我們應該回收哪些對象的問題,但是它顯然還不能承擔垃圾搜集的重任,因為我們在程序(程序也就是指我們運行在JVM上的JAVA程序)運行期間如果想進行垃圾回收,就必須讓GC線程與程序當中的線程互相配合,才能在不影響程序運行的前提下,順利的將垃圾進行回收。
為了達到這個目的,標記/清除算法就應運而生了。它的做法是當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被成為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
下面LZ具體解釋一下標記和清除分別都會做些什么。
標記:標記的過程其實就是,遍歷所有的GC Roots,然后將所有GC Roots可達的對象標記為存活的對象。
清除:清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。
其實這兩個步驟并不是特別復雜,也很容易理解。LZ用通俗的話解釋一下標記/清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發(fā)并將程序暫停,隨后將依舊存活的對象標記一遍,最終再將堆中所有沒被標記的對象全部清除掉,接下來便讓程序恢復運行。
下面LZ給各位制作了一組描述上面過程的圖片,結合著圖片,我們來直觀的看下這一過程,首先是第一張圖。
這張圖代表的是程序運行期間所有對象的狀態(tài),它們的標志位全部是0(也就是未標記,以下默認0就是未標記,1為已標記),假設這會兒有效內存空間耗盡了,JVM將會停止應用程序的運行并開啟GC線程,然后開始進行標記工作,按照根搜索算法,標記完以后,對象的狀態(tài)如下圖。
可以看到,按照根搜索算法,所有從root對象可達的對象就被標記為了存活的對象,此時已經(jīng)完成了第一階段標記。接下來,就要執(zhí)行第二階段清除了,那么清除完以后,剩下的對象以及對象的狀態(tài)如下圖所示。
可以看到,沒有被標記的對象將會回收清除掉,而被標記的對象將會留下,并且會將標記位重新歸0。接下來就不用說了,喚醒停止的程序線程,讓程序繼續(xù)運行即可。
其實這一過程并不復雜,甚至可以說非常簡單,各位說對嗎。不過其中有一點值得LZ一提,就是為什么非要停止程序的運行呢?
這個其實也不難理解,LZ舉個最簡單的例子,假設我們的程序與GC線程是一起運行的,各位試想這樣一種場景。
假設我們剛標記完圖中最右邊的那個對象,暫且記為A,結果此時在程序當中又new了一個新對象B,且A對象可以到達B對象。但是由于此時A對象已經(jīng)標記結束,B對象此時的標記位依然是0,因為它錯過了標記階段。因此當接下來輪到清除階段的時候,新對象B將會被苦逼的清除掉。如此一來,不難想象結果,GC線程將會導致程序無法正常工作。
上面的結果當然令人無法接受,我們剛new了一個對象,結果經(jīng)過一次GC,忽然變成null了,這還怎么玩?
到此為止,標記/清除算法LZ已經(jīng)介紹完了,下面我們來看下它的缺點,其實了解完它的算法原理,它的缺點就很好理解了。
1、首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用程序,這會導致用戶體驗非常差勁,尤其對于交互式的應用程序來說簡直是無法接受。試想一下,如果你玩一個網(wǎng)站,這個網(wǎng)站一個小時就掛五分鐘,你還玩嗎?
2、第二點主要的缺點,則是這種方式清理出來的空閑內存是不連續(xù)的,這點不難理解,我們的死亡對象都是隨即的出現(xiàn)在內存的各個角落的,現(xiàn)在把它們清除之后,內存的布局自然會亂七八糟。而為了應付這一點,JVM就不得不維持一個內存的空閑列表,這又是一種開銷。而且在分配數(shù)組對象的時候,尋找連續(xù)的內存空間會不太好找。
3、compacting算法(Compacting Collector) 標記-整理
標記/整理算法與標記/清除算法非常相似,它也是分為兩個階段:標記和整理。下面LZ給各位介紹一下這兩個階段都做了什么。
標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷GC Roots,然后將存活的對象標記。
整理:移動所有存活的對象,且按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收。因此,第二階段才稱為整理階段。
它GC前后的圖示與復制算法的圖非常相似,只不過沒有了活動區(qū)間和空閑區(qū)間的區(qū)別,而過程又與標記/清除算法非常相似,我們來看GC前內存中對象的狀態(tài)與布局,如下圖所示。
這張圖其實與標記/清楚算法一模一樣,只是LZ為了方便表示內存規(guī)則的連續(xù)排列,加了一個矩形表示內存區(qū)域。倘若此時GC線程開始工作,那么緊接著開始的就是標記階段了。此階段與標記/清除算法的標記階段是一樣一樣的,我們看標記階段過后對象的狀態(tài),如下圖。
沒什么可解釋的,接下來,便應該是整理階段了。我們來看當整理階段處理完以后,內存的布局是如何的,如下圖。
可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
不難看出,標記/整理算法不僅可以彌補標記/清除算法當中,內存區(qū)域分散的缺點,也消除了復制算法當中,內存減半的高額代價,可謂是一舉兩得,一箭雙雕,一石兩鳥,一。。。。一女兩男?
不過任何算法都會有其缺點,標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。從效率上來說,標記/整理算法要低于復制算法。
4、copying算法(Coping Collector) 復制
我們首先一起來看一下復制算法的做法,復制算法將內存劃分為兩個區(qū)間,在任意時間點,所有動態(tài)分配的對象都只能分配在其中一個區(qū)間(稱為活動區(qū)間),而另外一個區(qū)間(稱為空閑區(qū)間)則是空閑的。
當有效內存空間耗盡時,JVM將暫停程序運行,開啟復制算法GC線程。接下來GC線程會將活動區(qū)間內的存活對象,全部復制到空閑區(qū)間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。
此時,空閑區(qū)間已經(jīng)與活動區(qū)間交換,而垃圾對象現(xiàn)在已經(jīng)全部留在了原來的活動區(qū)間,也就是現(xiàn)在的空閑區(qū)間。事實上,在活動區(qū)間轉換為空間區(qū)間的同時,垃圾對象已經(jīng)被一次性全部回收。
聽起來復雜嗎?
其實一點也不復雜,有了上一章的基礎,相信各位理解這個算法不會費太多力氣。LZ給各位繪制一幅圖來說明問題,如下所示。
其實這個圖依然是上一章的例子,只不過此時內存被復制算法分成了兩部分,下面我們看下當復制算法的GC線程處理之后,兩個區(qū)域會變成什么樣子,如下所示。
可以看到,1和4號對象被清除了,而2、3、5、6號對象則是規(guī)則的排列在剛才的空閑區(qū)間,也就是現(xiàn)在的活動區(qū)間之內。此時左半部分已經(jīng)變成了空閑區(qū)間,不難想象,在下一次GC之后,左邊將會再次變成活動區(qū)間。
很明顯,復制算法彌補了標記/清除算法中,內存布局混亂的缺點。不過與此同時,它的缺點也是相當明顯的。
1、它浪費了一半的內存,這太要命了。
2、如果對象的存活率很高,我們可以極端一點,假設是100%存活,那么我們需要將所有對象都復制一遍,并將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。
所以從以上描述不難看出,復制算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內存的浪費。
算法總結
這里LZ給各位總結一下三個算法的共同點以及它們各自的優(yōu)勢劣勢,讓各位對比一下,想必會更加清晰。
它們的共同點主要有以下兩點。
1、三個算法都基于根搜索算法去判斷一個對象是否應該被回收,而支撐根搜索算法可以正常工作的理論依據(jù),就是語法中變量作用域的相關內容。因此,要想防止內存泄露,最根本的辦法就是掌握好變量作用域,而不應該使用前面內存管理雜談一章中所提到的C/C++式內存管理方式。
2、在GC線程開啟時,或者說GC過程開始時,它們都要暫停應用程序(stop the world)。
它們的區(qū)別LZ按照下面幾點來給各位展示。(>表示前者要優(yōu)于后者,=表示兩者效果一樣)
效率:復制算法>標記/整理算法>標記/清除算法(此處的效率只是簡單的對比時間復雜度,實際情況不一定如此)。
內存整齊度:復制算法=標記/整理算法>標記/清除算法。
內存利用率:標記/整理算法=標記/清除算法>復制算法。
可以看到標記/清除算法是比較落后的算法了,但是后兩種算法卻是在此基礎上建立的,俗話說“吃水不忘挖井人”,因此各位也莫要忘記了標記/清除這一算法前輩。而且,在某些時候,標記/清除也會有用武之地。
5、分代搜集算法
stop-and-copy垃圾收集器的一個缺陷是收集器必須復制所有的活動對象,這增加了程序等待時間,這是coping算法低效的原因。在程序設計中有這樣的規(guī)律:多數(shù)對象存在的時間比較短,少數(shù)的存在時間比較長。因此,generation算法將堆分成兩個或多個,每個子堆作為對象的一代 (generation)。由于多數(shù)對象存在的時間比較短,隨著程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行后,上次運行存活下來的對象移到下一最高代的子堆中,由于老一代的子堆不會經(jīng)常被回收,因而節(jié)省了時間。
對象分類
分代搜集算法是針對對象的不同特性,而使用適合的算法,這里面并沒有實際上的新算法產(chǎn)生。與其說分代搜集算法是第四個算法,不如說它是對前三個算法的實際應用。
首先我們來探討一下對象的不同特性,接下來LZ和各位來一起給這些對象選擇GC算法。
內存中的對象按照生命周期的長短大致可以分為三種,以下命名均為LZ個人的命名。
1、夭折對象:朝生夕滅的對象,通俗點講就是活不了多久就得死的對象。
例子:某一個方法的局域變量、循環(huán)內的臨時變量等等。
2、老不死對象:這類對象一般活的比較久,歲數(shù)很大還不死,但歸根結底,老不死對象也幾乎早晚要死的,但也只是幾乎而已。
例子:緩存對象、數(shù)據(jù)庫連接對象、單例對象(單例模式)等等。
3、不滅對象:此類對象一般一旦出生就幾乎不死了,它們幾乎會一直永生不滅,記得,只是幾乎不滅而已。
例子:String池中的對象(享元模式)、加載過的類信息等等。
對象對應的內存區(qū)域
還記得前面介紹內存管理時,JVM對內存的劃分嗎?
我們將上面三種對象對應到內存區(qū)域當中,就是夭折對象和老不死對象都在JAVA堆,而不滅對象在方法區(qū)。
之前的一章中我們就已經(jīng)說過,對于JAVA堆,JVM規(guī)范要求必須實現(xiàn)GC,因而對于夭折對象和老不死對象來說,死幾乎是必然的結局,但也只是幾乎,還是難免會有一些對象會一直存活到應用結束。然而JVM規(guī)范對方法區(qū)的GC并不做要求,所以假設一個JVM實現(xiàn)沒有對方法區(qū)實現(xiàn)GC,那么不滅對象就是真的不滅對象了。
由于不滅對象的生命周期過長,因此分代搜集算法就是針對的JAVA堆而設計的,也就是針對夭折對象和老不死對象。
JAVA堆的對象回收(夭折對象和老不死對象)
有了以上分析,我們來看看分代搜集算法如何處理JAVA堆的內存回收的,也就是夭折對象與老不死對象的回收。
夭折對象:這類對象朝生夕滅,存活時間短,還記得復制算法的使用要求嗎?那就是對象存活率不能太高,因此夭折對象是最適合使用復制算法的。
小疑問:50%內存的浪費怎么辦?
答疑:因為夭折對象一般存活率較低,因此可以不使用50%的內存作為空閑,一般的,使用兩塊10%的內存作為空閑和活動區(qū)間,而另外80%的內存,則是用來給新建對象分配內存的。一旦發(fā)生GC,將10%的活動區(qū)間與另外80%中存活的對象轉移到10%的空閑區(qū)間,接下來,將之前90%的內存全部釋放,以此類推。
為了讓各位更加清楚的看出來這個GC流程,LZ給出下面圖示。
圖中標注了三個區(qū)域中在各個階段,各自內存的情況。相信看著圖,它的GC流程已經(jīng)不難理解了。
不過有兩點LZ需要提一下,第一點是使用這樣的方式,我們只浪費了10%的內存,這個是可以接受的,因為我們換來了內存的整齊排列與GC速度。第二點是,這個策略的前提是,每次存活的對象占用的內存不能超過這10%的大小,一旦超過,多出的對象將無法復制。
為了解決上面的意外情況,也就是存活對象占用的內存太大時的情況,高手們將JAVA堆分成兩部分來處理,上述三個區(qū)域則是第一部分,稱為新生代或者年輕代。而余下的一部分,專門存放老不死對象的則稱為年老代。
是不是很貼切的名字呢?下面我們看看老不死對象的處理方式。
老不死對象:這一類對象存活率非常高,因為它們大多是從新生代轉過來的。就像人一樣,活的年月久了,就變成老不死了。
通常情況下,以下兩種情況發(fā)生的時候,對象會從新生代區(qū)域轉到年老帶區(qū)域。
1、在新生代里的每一個對象,都會有一個年齡,當這些對象的年齡到達一定程度時(年齡就是熬過的GC次數(shù),每次GC如果對象存活下來,則年齡加1),則會被轉到年老代,而這個轉入年老代的年齡值,一般在JVM中是可以設置的。
2、在新生代存活對象占用的內存超過10%時,則多余的對象會放入年老代。這種時候,年老代就是新生代的“備用倉庫”。
針對老不死對象的特性,顯然不再適合使用復制算法,因為它的存活率太高,而且不要忘了,如果年老代再使用復制算法,它可是沒有備用倉庫的。因此一般針對老不死對象只能采用標記/整理或者標記/清除算法。
方法區(qū)的對象回收(不滅對象)
以上兩種情況已經(jīng)解決了GC的大部分問題,因為JAVA堆是GC的主要關注對象,而以上也已經(jīng)包含了分代搜集算法的全部內容,接下來對于不滅對象的回收,已經(jīng)不屬于分代搜集算法的內容。
不滅對象存在于方法區(qū),在我們常用的hotspot虛擬機(JDK默認的JVM)中,方法區(qū)也被親切的稱為永久代,又是一個很貼切的名字不是嗎?
其實在很久很久以前,是不存在永久代的。當時永久代與年老代都存放在一起,里面包含了JAVA類的實例信息以及類信息。但是后來發(fā)現(xiàn),對于類信息的卸載幾乎很少發(fā)生,因此便將二者分離開來。幸運的是,這樣做確實提高了不少性能。于是永久代便被拆分出來了。
這一部分區(qū)域的GC與年老代采用相似的方法,由于都沒有“備用倉庫”,二者都是只能使用標記/清除和標記/整理算法。
回收的時機
JVM在進行GC時,并非每次都對上面三個內存區(qū)域一起回收的,大部分時候回收的都是指新生代。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC?or Full GC),它們所針對的區(qū)域如下。
普通GC(minor GC):只針對新生代區(qū)域的GC。
全局GC(major GC or Full GC):針對年老代的GC,偶爾伴隨對新生代的GC以及對永久代的GC。
由于年老代與永久代相對來說GC效果不好,而且二者的內存使用增長速度也慢,因此一般情況下,需要經(jīng)過好幾次普通GC,才會觸發(fā)一次全局GC。
三、垃圾收集器:
通俗的講,使用編程語言將算法實現(xiàn)出來,產(chǎn)生的程序就是垃圾搜集器了。既然談到了編程語言的實現(xiàn),那么在討論垃圾搜集器的時候,就已經(jīng)涉及到具體的虛擬機實現(xiàn)了。
或許有不少做JAVA開發(fā)的猿友還不知道,我們平時使用的JDK中,默認的JVM是hotspot,換句話說,我們大部分時候使用的JVM都是hotspot的實現(xiàn)版本,因此,本次LZ討論垃圾搜集器都是基于hotspot版JVM來進行的,請各位猿友要知曉這一點。
更直觀的,我們可以在我們平時開發(fā)的機子上,輸入java -version來查看JVM的版本,相信大部分猿友對這個命令都不陌生吧,LZ的機子截圖如下。
垃圾搜集器的分類
上面我們已經(jīng)提到,垃圾搜集器實際就是算法的編程語言實現(xiàn)。既然牽扯到編程語言,那么必然離不開線程,而且我們在前面講解算法的時候也一直假設是一條GC線程在做著GC的事情。
因此,垃圾搜集器大致分為以下三類。
串行搜集器(serial collector):它只有一條GC線程,且就像前面說的,它在運行的時候需要暫停用戶程序(stop the world)。
并行搜集器(parallel collector):它有多條GC線程,且它也需要暫停用戶程序(stop the world)。
并發(fā)搜集器(concurrent collector):它有一條或多條GC線程,且它需要在部分階段暫停用戶程序(stop the world),部分階段與用戶程序并發(fā)執(zhí)行。
并發(fā)(concurrent)與并行(parallel)
看完上面的定義,相信有一部分猿友已經(jīng)蒙了,一會單線程,一會多線程,一會串行,一會并行,一會并發(fā),這都神馬玩意?
單線程和多線程就不必多說了,這個很好理解,串行與并行也比較好理解,難于分辨的就是并行(parallel)與并發(fā)(concurrent)。
對于很多有關并發(fā)的解釋,LZ覺得有一個最貼切。它是這么解釋的,并發(fā)就是兩個任務A和B需要相互獨立的運行,并且A任務先開始后,B任務在A任務結束之前開始了。
并發(fā)本身是比較好理解的,那么它與并行的關系與區(qū)別是什么呢?
事實上,并行是并發(fā)的一種實現(xiàn)方式。LZ覺得這么說各位可能會更好理解,當然,并行并不是并發(fā)的唯一實現(xiàn)方式,還有一種就是我們所熟悉的時間片切換。也就是A任務執(zhí)行一會,B任務執(zhí)行一會,交替執(zhí)行。
并行必須在多核多處理器或者分布式系統(tǒng)(本質還是多核多處理器)的前提下才能發(fā)生,而交替執(zhí)行或者說時間片切換是在單核的處理器上發(fā)生的。
hotspot中的垃圾搜集器
我們上面已經(jīng)簡單探討了垃圾搜集器的分類,在hotspotJVM中,每一個種類的垃圾搜集器都有對應的實現(xiàn),如下。
串行搜集器的實現(xiàn):serial(用于新生代,采用復制算法)、serial old(用于年老代,采用標記/整理算法)
并行搜集器的實現(xiàn):ParNew(用于新生代,采用復制算法)、Parallel Scavenge(用于新生代,采用復制算法)、Parallel old(用于年老代,采用標記/整理算法)
并發(fā)搜集器的實現(xiàn):concurrent mark sweep[CMS](用于年老代,采用標記/清除算法)
可以看到,上面每一種垃圾搜集器都是針對不同內存區(qū)域所設計的,因為它們采用的算法不同,凡是用于新生代的都是使用的復制算法,而用于年老代的都是使用的標記/清除或者標記/整理算法。
在實際應用中,我們需要給JVM的新生代和年老代分別選擇垃圾搜集器,可以看到無論是新生代還是年老代都分別有三種實現(xiàn),換句話說,我們應該有3*3=9種選擇。但是,事實并非如此。
事實上,這六種垃圾搜集器只有六種選擇,因為有的垃圾搜集器由于具體實現(xiàn)的方式等一系列原因無法在一起工作,如下圖。
針對上圖,紅的就是串行搜集器,綠的是并行搜集器,唯一一個黃的是并發(fā)搜集器。上面三個是新生代的搜集器,下面三個是年老代的搜集器。兩者之間有連線,則表示兩者可以配合工作。
這六種組合并沒有說哪個組合最強,哪個組合最弱,還是那句話,只有最合適的,沒有最好的。因此這就需要我們對每一種組合有一定的認識,才能在使用的時候選擇更適合的垃圾搜集器
關于垃圾搜集器:JVM內存管理------垃圾搜集器精解(讓你在垃圾搜集器的世界里耍的游刃有余) - 左瀟龍 - 博客園、JVM內存管理------垃圾搜集器參數(shù)精解 - 左瀟龍 - 博客園
五、finalize()方法
在JVM垃圾回收器收集一個對象之前,一般要求程序調用適當?shù)姆椒ㄡ尫刨Y源,但在沒有明確釋放資源的情況下,Java提供了缺省機制來終止該對象心釋放資源,這個方法就是finalize()。它的原型為:
protected void finalize() throws Throwable
在finalize()方法返回之后,對象消失,垃圾收集開始執(zhí)行。原型中的throws Throwable表示它可以拋出任何類型的異常。
之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況。假定你的對象(并非使用new方法)獲得了一塊“特殊”的內存區(qū)域,由于垃圾回收器只知道那些顯示地經(jīng)由new分配的內存空間,所以它不知道該如何釋放這塊“特殊”的內存區(qū)域,那么這個時候java允許在類中定義一個由finalize()方法。
特殊的區(qū)域例如:1)由于在分配內存的時候可能采用了類似 C語言的做法,而非JAVA的通常new做法。這種情況主要發(fā)生在native method中,比如native method調用了C/C++方法malloc()函數(shù)系列來分配存儲空間,但是除非調用free()函數(shù),否則這些內存空間將不會得到釋放,那么這個時候就可能造成內存泄漏。但是由于free()方法是在C/C++中的函數(shù),所以finalize()中可以用本地方法來調用它。以釋放這些“特殊”的內存空間。2)又或者打開的文件資源,這些資源不屬于垃圾回收器的回收范圍。
換言之,finalize()的主要用途是釋放一些其他做法開辟的內存空間,以及做一些清理工作。因為在JAVA中并沒有提夠像“析構”函數(shù)或者類似概念的函數(shù),要做一些類似清理工作的時候,必須自己動手創(chuàng)建一個執(zhí)行清理工作的普通方法,也就是override Object這個類中的finalize()方法。例如,假設某一個對象在創(chuàng)建過程中會將自己繪制到屏幕上,如果不是明確地從屏幕上將其擦出,它可能永遠都不會被清理。如果在finalize()加入某一種擦除功能,當GC工作時,finalize()得到了調用,圖像就會被擦除。要是GC沒有發(fā)生,那么這個圖像就會
被一直保存下來。
一旦垃圾回收器準備好釋放對象占用的存儲空間,首先會去調用finalize()方法進行一些必要的清理工作。只有到下一次再進行垃圾回收動作的時候,才會真正釋放這個對象所占用的內存空間。
在普通的清除工作中,為清除一個對象,那個對象的用戶必須在希望進行清除的地點調用一個清除方法。這與C++"析構函數(shù)"的概念稍有抵觸。在C++中,所有對象都會破壞(清除)。或者換句話說,所有對象都"應該"破壞。若將C++對象創(chuàng)建成一個本地對象,比如在堆棧中創(chuàng)建(在Java中是不可能的,Java都在堆中),那么清除或破壞工作就會在"結束花括號"所代表的、創(chuàng)建這個對象的作用域的末尾進行。若對象是用new創(chuàng)建的(類似于Java),那么當程序員調用C++的 delete命令時(Java沒有這個命令),就會調用相應的析構函數(shù)。若程序員忘記了,那么永遠不會調用析構函數(shù),我們最終得到的將是一個內存"漏洞",另外還包括對象的其他部分永遠不會得到清除。
相反,Java不允許我們創(chuàng)建本地(局部)對象--無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對象,因為垃圾回收器會幫助我們自動釋放存儲空間。所以如果站在比較簡化的立場,我們可以說正是由于存在垃圾回收機制,所以Java沒有析構函數(shù)。然而,隨著以后學習的深入,就會知道垃圾收集器的存在并不能完全消除對析構函數(shù)的需要,或者說不能消除對析構函數(shù)代表的那種機制的需要(原因見下一段。另外finalize()函數(shù)是在垃圾回收器準備釋放對象占用的存儲空間的時候被調用的,絕對不能直接調用finalize(),所以應盡量避免用它)。若希望執(zhí)行除釋放存儲空間之外的其他某種形式的清除工作,仍然必須調用Java中的一個方法。它等價于C++的析構函數(shù),只是沒后者方便。
在C++中所有的對象運用delete()一定會被銷毀,而JAVA里的對象并非總會被垃圾回收器回收。In another word, 1 對象可能不被垃圾回收,2 垃圾回收并不等于“析構”,3 垃圾回收只與內存有關。也就是說,并不是如果一個對象不再被使用,是不是要在finalize()中釋放這個對象中含有的其它對象呢?不是的。因為無論對象是如何創(chuàng)建的,垃圾回收器都會負責釋放那些對象占有的內存。
六、觸發(fā)主GC(Garbage Collector)的條件
JVM進行次GC的頻率很高,但因為這種GC占用時間極短,所以對系統(tǒng)產(chǎn)生的影響不大。更值得關注的是主GC的觸發(fā)條件,因為它對系統(tǒng)影響很明顯。總的來說,有兩個條件會觸發(fā)主GC:
1)當應用程序空閑時,即沒有應用線程在運行時,GC會被調用。因為GC在優(yōu)先級最低的線程中進行,所以當應用忙時,GC線程就不會被調用,但以下條件除外。
2)Java堆內存不足時,GC會被調用。當應用線程在運行,并在運行過程中創(chuàng)建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用于新的分配。若GC一次之后仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則 JVM將報“out of memory”的錯誤,Java應用將停止。
由于是否進行主GC由JVM根據(jù)系統(tǒng)環(huán)境決定,而系統(tǒng)環(huán)境在不斷的變化當中,所以主GC的運行具有不確定性,無法預計它何時必然出現(xiàn),但可以確定的是對一個長期運行的應用來說,其主GC是反復進行的。
七、減少GC開銷的措施
根據(jù)上述GC的機制,程序的運行會直接影響系統(tǒng)環(huán)境的變化,從而影響GC的觸發(fā)。若不針對GC的特點進行設計和編碼,就會出現(xiàn)內存駐留等一系列負面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:
(1)不要顯式調用System.gc()
此函數(shù)建議JVM進行主GC,雖然只是建議而非一定,但很多情況下它會觸發(fā)主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數(shù)。
(2)盡量減少臨時對象的使用
臨時對象在跳出函數(shù)調用后,會成為垃圾,少用臨時變量就相當于減少了垃圾的產(chǎn)生,從而延長了出現(xiàn)上述第二個觸發(fā)條件出現(xiàn)的時間,減少了主GC的機會。
(3)對象不用時最好顯式置為Null
一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利于GC收集器判定垃圾,從而提高了GC的效率。
(4)盡量使用StringBuffer,而不用String來累加字符串
由于String是固定長的字符串對象,累加String對象時,并非在一個String對象中擴增,而是重新創(chuàng)建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執(zhí)行過程中會產(chǎn)生多個垃圾對象,因為對次作“+”操作時都必須創(chuàng)建新的String對象,但這些過渡對象對系統(tǒng)來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產(chǎn)生中間對象。
(5)能用基本類型如Int,Long,就不用Integer,Long對象
基本類型變量占用的內存資源比相應對象占用的少得多,如果沒有必要,最好使用基本變量。
(6)盡量少用靜態(tài)對象變量
靜態(tài)變量屬于全局變量,不會被GC回收,它們會一直占用內存。
(7)分散對象創(chuàng)建或刪除的時間
集中在短時間內大量創(chuàng)建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現(xiàn)了大量的垃圾對象,空閑空間必然減少,從而大大增加了下一次創(chuàng)建新對象時強制主GC的機會。
八、關于垃圾回收的幾點補充
經(jīng)過上述的說明,可以發(fā)現(xiàn)垃圾回收有以下的幾個特點:
(1)垃圾收集發(fā)生的不可預知性:由于實現(xiàn)了不同的垃圾回收算法和采用了不同的收集機制,所以它有可能是定時發(fā)生,有可能是當出現(xiàn)系統(tǒng)空閑CPU資源時發(fā)生,也有可能是和原始的垃圾收集一樣,等到內存消耗出現(xiàn)極限時發(fā)生,這與垃圾收集器的選擇和具體的設置都有關系。
(2)垃圾收集的精確性:主要包括2 個方面:(a)垃圾收集器能夠精確標記活著的對象;(b)垃圾收集器能夠精確地定位對象之間的引用關系。前者是完全地回收所有廢棄對象的前提,否則就可能造成內存泄漏。而后者則是實現(xiàn)歸并和復制等算法的必要條件。所有不可達對象都能夠可靠地得到回收,所有對象都能夠重新分配,允許對象的復制和對象內存的縮并,這樣就有效地防止內存的支離破碎。
(3)現(xiàn)在有許多種不同的垃圾收集器,每種有其算法且其表現(xiàn)各異,既有當垃圾收集開始時就停止應用程序的運行,又有當垃圾收集開始時也允許應用程序的線程運行,還有在同一時間垃圾收集多線程運行。
(4)垃圾收集的實現(xiàn)和具體的JVM 以及JVM的內存模型有非常緊密的關系。不同的JVM 可能采用不同的垃圾收集,而JVM 的內存模型決定著該JVM可以采用哪些類型垃圾收集。現(xiàn)在,HotSpot 系列JVM中的內存系統(tǒng)都采用先進的面向對象的框架設計,這使得該系列JVM都可以采用最先進的垃圾收集。
(5)隨著技術的發(fā)展,現(xiàn)代垃圾收集技術提供許多可選的垃圾收集器,而且在配置每種收集器的時候又可以設置不同的參數(shù),這就使得根據(jù)不同的應用環(huán)境獲得最優(yōu)的應用性能成為可能。
針對以上特點,我們在使用的時候要注意:
(1)不要試圖去假定垃圾收集發(fā)生的時間,這一切都是未知的。比如,方法中的一個臨時對象在方法調用完畢后就變成了無用對象,這個時候它的內存就可以被釋放。
(2)Java中提供了一些和垃圾收集打交道的類,而且提供了一種強行執(zhí)行垃圾收集的方法--調用System.gc(),但這同樣是個不確定的方法。Java 中并不保證每次調用該方法就一定能夠啟動垃圾收集,它只不過會向JVM發(fā)出這樣一個申請,到底是否真正執(zhí)行垃圾收集,一切都是個未知數(shù)。
(3)挑選適合自己的垃圾收集器。一般來說,如果系統(tǒng)沒有特殊和苛刻的性能要求,可以采用JVM的缺省選項。否則可以考慮使用有針對性的垃圾收集器,比如增量收集器就比較適合實時性要求較高的系統(tǒng)之中。系統(tǒng)具有較高的配置,有比較多的閑置資源,可以考慮使用并行標記/清除收集器。
(4)關鍵的也是難把握的問題是內存泄漏。良好的編程習慣和嚴謹?shù)木幊虘B(tài)度永遠是最重要的,不要讓自己的一個小錯誤導致內存出現(xiàn)大漏洞。
(5)盡早釋放無用對象的引用。大多數(shù)程序員在使用臨時變量的時候,都是讓引用變量在退出活動域(scope)后,自動設置為null,暗示垃圾收集器來收集該對象,還必須注意該引用的對象是否被監(jiān)聽,如果有,則要去掉監(jiān)聽器,然后再賦空值。
參考: