虛擬機(jī)(四)-JVM垃圾回收

一. 垃圾回收的意義

? ? ? 在C++中,對(duì)象所占的內(nèi)存在程序結(jié)束運(yùn)行之前一直被占用,在明確釋放之前不能分配給其它對(duì)象;而在Java中,當(dāng)沒有對(duì)象引用指向原先分配給某個(gè)對(duì)象的內(nèi)存時(shí),該內(nèi)存便成為垃圾。JVM的一個(gè)系統(tǒng)級(jí)線程會(huì)自動(dòng)釋放該內(nèi)存塊。垃圾回收意味著程序不再需要的對(duì)象是"無用信息",這些信息將被丟棄。當(dāng)一個(gè)對(duì)象不再被引用的時(shí)候,內(nèi)存回收它占領(lǐng)的空間,以便空間被后來的新對(duì)象使用。事實(shí)上,除了釋放沒用的對(duì)象,垃圾回收也可以清除內(nèi)存記錄碎片。由于創(chuàng)建對(duì)象和垃圾回收器釋放丟棄對(duì)象所占的內(nèi)存空間,內(nèi)存會(huì)出現(xiàn)碎片。碎片是分配給對(duì)象的內(nèi)存塊之間的空閑內(nèi)存洞。碎片整理將所占用的堆內(nèi)存移到堆的一端,JVM將整理出的內(nèi)存分配給新的對(duì)象。

垃圾回收能自動(dòng)釋放內(nèi)存空間,減輕編程的負(fù)擔(dān)。這使Java 虛擬機(jī)具有一些優(yōu)點(diǎn)。首先,它能使編程效率提高。在沒有垃圾回收機(jī)制的時(shí)候,可能要花許多時(shí)間來解決一個(gè)難懂的存儲(chǔ)器問題。在用Java語言編程的時(shí)候,靠垃圾回收機(jī)制可大大縮短時(shí)間。其次是它保護(hù)程序的完整性, 垃圾回收是Java語言安全性策略的一個(gè)重要部份。

垃圾回收的一個(gè)潛在的缺點(diǎn)是它的開銷影響程序性能。Java虛擬機(jī)必須追蹤運(yùn)行程序中有用的對(duì)象,而且最終釋放沒用的對(duì)象。這一個(gè)過程需要花費(fèi)處理器的時(shí)間。其次垃圾回收算法的不完備性,早先采用的某些垃圾回收算法就不能保證100%收集到所有的廢棄內(nèi)存。當(dāng)然隨著垃圾回收算法的不斷改進(jìn)以及軟硬件運(yùn)行效率的不斷提升,這些問題都可以迎刃而解。

二、垃圾收集的算法分析

? ? ?既然是要進(jìn)行自動(dòng)GC,那必然會(huì)有相應(yīng)的策略,而這些策略解決了哪些問題呢,粗略的來說,主要有以下幾點(diǎn)。

1、哪些對(duì)象可以被回收。

2、何時(shí)回收這些對(duì)象。

3、采用什么樣的方式回收。

具體算法:

1、引用計(jì)數(shù)法(Reference Counting Collector)

? ? ? 引用計(jì)數(shù)法是唯一沒有使用根集的垃圾回收的法,該算法使用引用計(jì)數(shù)器來區(qū)分存活對(duì)象和不再使用的對(duì)象。一般來說,堆中的每個(gè)對(duì)象對(duì)應(yīng)一個(gè)引用計(jì)數(shù)器。當(dāng)每一次創(chuàng)建一個(gè)對(duì)象并賦給一個(gè)變量時(shí),引用計(jì)數(shù)器置為1。當(dāng)對(duì)象被賦給任意變量時(shí),引用計(jì)數(shù)器每次加1當(dāng)對(duì)象出了作用域后(該對(duì)象丟棄不再使用),引用計(jì)數(shù)器減1,一旦引用計(jì)數(shù)器為0,對(duì)象就滿足了垃圾收集的條件。

基于引用計(jì)數(shù)器的垃圾收集器運(yùn)行較快,但是有個(gè)致命缺陷:那就是對(duì)于循環(huán)引用的對(duì)象無法進(jì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);

}

}

? ? ? 標(biāo)注了1、2、3三個(gè)數(shù)字,當(dāng)?shù)?個(gè)地方的語句執(zhí)行完以后,兩個(gè)對(duì)象的引用計(jì)數(shù)全部為1。當(dāng)?shù)?個(gè)地方的語句執(zhí)行完以后,兩個(gè)對(duì)象的引用計(jì)數(shù)就全部變成了2。當(dāng)?shù)?個(gè)地方的語句執(zhí)行完以后,也就是將二者全部歸為空值以后,二者的引用計(jì)數(shù)仍然為1。根據(jù)引用計(jì)數(shù)算法的回收規(guī)則,引用計(jì)數(shù)沒有歸0的時(shí)候是不會(huì)被回收的。

根搜索算法

? ? ? 大多數(shù)垃圾回收算法使用了根集(root set)這個(gè)概念;所謂根集就是正在執(zhí)行的Java程序可以訪問的引用變量的集合(包括局部變量、參數(shù)、類變量),程序可以使用引用變量訪問對(duì)象的屬性和調(diào)用對(duì)象的方法。垃圾回收首先需要確定從根開始哪些是可達(dá)的和哪些是不可達(dá)的,從根集可達(dá)的對(duì)象都是活動(dòng)對(duì)象,它們不能作為垃圾被回收,這也包括從根集間接可達(dá)的對(duì)象。而根集通過任意路徑不可達(dá)的對(duì)象符合垃圾收集的條件,應(yīng)該被回收。下面介紹幾個(gè)常用的算法。說到GC roots(GC根),在JAVA語言中,可以當(dāng)做GC roots的對(duì)象有以下幾種:1、虛擬機(jī)棧中的引用的對(duì)象。2、方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。3、方法區(qū)中的常量引用的對(duì)象。4、本地方法棧中JNI的引用的對(duì)象。

第一和第四種都是指的方法的本地變量表,第二種表達(dá)的意思比較清晰,第三種主要指的是聲明為final的常量值

2、tracing算法(Tracing Collector) 標(biāo)記-清除

根搜索算法,它可以解決我們應(yīng)該回收哪些對(duì)象的問題,但是它顯然還不能承擔(dān)垃圾搜集的重任,因?yàn)槲覀冊(cè)诔绦颍ǔ绦蛞簿褪侵肝覀冞\(yùn)行在JVM上的JAVA程序)運(yùn)行期間如果想進(jìn)行垃圾回收,就必須讓GC線程與程序當(dāng)中的線程互相配合,才能在不影響程序運(yùn)行的前提下,順利的將垃圾進(jìn)行回收。

為了達(dá)到這個(gè)目的,標(biāo)記/清除算法就應(yīng)運(yùn)而生了。它的做法是當(dāng)堆中的有效內(nèi)存空間(available memory)被耗盡的時(shí)候,就會(huì)停止整個(gè)程序(也被成為stop the world),然后進(jìn)行兩項(xiàng)工作,第一項(xiàng)則是標(biāo)記,第二項(xiàng)則是清除。

下面LZ具體解釋一下標(biāo)記和清除分別都會(huì)做些什么。

標(biāo)記:標(biāo)記的過程其實(shí)就是,遍歷所有的GC Roots,然后將所有GC Roots可達(dá)的對(duì)象標(biāo)記為存活的對(duì)象。

清除:清除的過程將遍歷堆中所有的對(duì)象,將沒有標(biāo)記的對(duì)象全部清除掉。

其實(shí)這兩個(gè)步驟并不是特別復(fù)雜,也很容易理解。LZ用通俗的話解釋一下標(biāo)記/清除算法,就是當(dāng)程序運(yùn)行期間,若可以使用的內(nèi)存被耗盡的時(shí)候,GC線程就會(huì)被觸發(fā)并將程序暫停,隨后將依舊存活的對(duì)象標(biāo)記一遍,最終再將堆中所有沒被標(biāo)記的對(duì)象全部清除掉,接下來便讓程序恢復(fù)運(yùn)行。

下面LZ給各位制作了一組描述上面過程的圖片,結(jié)合著圖片,我們來直觀的看下這一過程,首先是第一張圖。

這張圖代表的是程序運(yùn)行期間所有對(duì)象的狀態(tài),它們的標(biāo)志位全部是0(也就是未標(biāo)記,以下默認(rèn)0就是未標(biāo)記,1為已標(biāo)記),假設(shè)這會(huì)兒有效內(nèi)存空間耗盡了,JVM將會(huì)停止應(yīng)用程序的運(yùn)行并開啟GC線程,然后開始進(jìn)行標(biāo)記工作,按照根搜索算法,標(biāo)記完以后,對(duì)象的狀態(tài)如下圖。

可以看到,按照根搜索算法,所有從root對(duì)象可達(dá)的對(duì)象就被標(biāo)記為了存活的對(duì)象,此時(shí)已經(jīng)完成了第一階段標(biāo)記。接下來,就要執(zhí)行第二階段清除了,那么清除完以后,剩下的對(duì)象以及對(duì)象的狀態(tài)如下圖所示。

可以看到,沒有被標(biāo)記的對(duì)象將會(huì)回收清除掉,而被標(biāo)記的對(duì)象將會(huì)留下,并且會(huì)將標(biāo)記位重新歸0。接下來就不用說了,喚醒停止的程序線程,讓程序繼續(xù)運(yùn)行即可。

其實(shí)這一過程并不復(fù)雜,甚至可以說非常簡(jiǎn)單,各位說對(duì)嗎。不過其中有一點(diǎn)值得LZ一提,就是為什么非要停止程序的運(yùn)行呢?

這個(gè)其實(shí)也不難理解,LZ舉個(gè)最簡(jiǎn)單的例子,假設(shè)我們的程序與GC線程是一起運(yùn)行的,各位試想這樣一種場(chǎng)景。

假設(shè)我們剛標(biāo)記完圖中最右邊的那個(gè)對(duì)象,暫且記為A,結(jié)果此時(shí)在程序當(dāng)中又new了一個(gè)新對(duì)象B,且A對(duì)象可以到達(dá)B對(duì)象。但是由于此時(shí)A對(duì)象已經(jīng)標(biāo)記結(jié)束,B對(duì)象此時(shí)的標(biāo)記位依然是0,因?yàn)樗e(cuò)過了標(biāo)記階段。因此當(dāng)接下來輪到清除階段的時(shí)候,新對(duì)象B將會(huì)被苦逼的清除掉。如此一來,不難想象結(jié)果,GC線程將會(huì)導(dǎo)致程序無法正常工作。

上面的結(jié)果當(dāng)然令人無法接受,我們剛new了一個(gè)對(duì)象,結(jié)果經(jīng)過一次GC,忽然變成null了,這還怎么玩?

到此為止,標(biāo)記/清除算法LZ已經(jīng)介紹完了,下面我們來看下它的缺點(diǎn),其實(shí)了解完它的算法原理,它的缺點(diǎn)就很好理解了。

1、首先,它的缺點(diǎn)就是效率比較低(遞歸與全堆對(duì)象遍歷),而且在進(jìn)行GC的時(shí)候,需要停止應(yīng)用程序,這會(huì)導(dǎo)致用戶體驗(yàn)非常差勁,尤其對(duì)于交互式的應(yīng)用程序來說簡(jiǎn)直是無法接受。試想一下,如果你玩一個(gè)網(wǎng)站,這個(gè)網(wǎng)站一個(gè)小時(shí)就掛五分鐘,你還玩嗎?

2、第二點(diǎn)主要的缺點(diǎn),則是這種方式清理出來的空閑內(nèi)存是不連續(xù)的,這點(diǎn)不難理解,我們的死亡對(duì)象都是隨即的出現(xiàn)在內(nèi)存的各個(gè)角落的,現(xiàn)在把它們清除之后,內(nèi)存的布局自然會(huì)亂七八糟。而為了應(yīng)付這一點(diǎn),JVM就不得不維持一個(gè)內(nèi)存的空閑列表,這又是一種開銷。而且在分配數(shù)組對(duì)象的時(shí)候,尋找連續(xù)的內(nèi)存空間會(huì)不太好找。

3、compacting算法(Compacting Collector) 標(biāo)記-整理

標(biāo)記/整理算法與標(biāo)記/清除算法非常相似,它也是分為兩個(gè)階段:標(biāo)記和整理。下面LZ給各位介紹一下這兩個(gè)階段都做了什么。

標(biāo)記:它的第一個(gè)階段與標(biāo)記/清除算法是一模一樣的,均是遍歷GC Roots,然后將存活的對(duì)象標(biāo)記。

整理:移動(dòng)所有存活的對(duì)象,且按照內(nèi)存地址次序依次排列,然后將末端內(nèi)存地址以后的內(nèi)存全部回收。因此,第二階段才稱為整理階段。

它GC前后的圖示與復(fù)制算法的圖非常相似,只不過沒有了活動(dòng)區(qū)間和空閑區(qū)間的區(qū)別,而過程又與標(biāo)記/清除算法非常相似,我們來看GC前內(nèi)存中對(duì)象的狀態(tài)與布局,如下圖所示。

這張圖其實(shí)與標(biāo)記/清楚算法一模一樣,只是LZ為了方便表示內(nèi)存規(guī)則的連續(xù)排列,加了一個(gè)矩形表示內(nèi)存區(qū)域。倘若此時(shí)GC線程開始工作,那么緊接著開始的就是標(biāo)記階段了。此階段與標(biāo)記/清除算法的標(biāo)記階段是一樣一樣的,我們看標(biāo)記階段過后對(duì)象的狀態(tài),如下圖。

沒什么可解釋的,接下來,便應(yīng)該是整理階段了。我們來看當(dāng)整理階段處理完以后,內(nèi)存的布局是如何的,如下圖。

可以看到,標(biāo)記的存活對(duì)象將會(huì)被整理,按照內(nèi)存地址依次排列,而未被標(biāo)記的內(nèi)存會(huì)被清理掉。如此一來,當(dāng)我們需要給新對(duì)象分配內(nèi)存時(shí),JVM只需要持有一個(gè)內(nèi)存的起始地址即可,這比維護(hù)一個(gè)空閑列表顯然少了許多開銷。

不難看出,標(biāo)記/整理算法不僅可以彌補(bǔ)標(biāo)記/清除算法當(dāng)中,內(nèi)存區(qū)域分散的缺點(diǎn),也消除了復(fù)制算法當(dāng)中,內(nèi)存減半的高額代價(jià),可謂是一舉兩得,一箭雙雕,一石兩鳥,一。。。。一女兩男?

不過任何算法都會(huì)有其缺點(diǎn),標(biāo)記/整理算法唯一的缺點(diǎn)就是效率也不高,不僅要標(biāo)記所有存活對(duì)象,還要整理所有存活對(duì)象的引用地址。從效率上來說,標(biāo)記/整理算法要低于復(fù)制算法。

4、copying算法(Coping Collector) 復(fù)制

我們首先一起來看一下復(fù)制算法的做法,復(fù)制算法將內(nèi)存劃分為兩個(gè)區(qū)間,在任意時(shí)間點(diǎn),所有動(dòng)態(tài)分配的對(duì)象都只能分配在其中一個(gè)區(qū)間(稱為活動(dòng)區(qū)間),而另外一個(gè)區(qū)間(稱為空閑區(qū)間)則是空閑的。

當(dāng)有效內(nèi)存空間耗盡時(shí),JVM將暫停程序運(yùn)行,開啟復(fù)制算法GC線程。接下來GC線程會(huì)將活動(dòng)區(qū)間內(nèi)的存活對(duì)象,全部復(fù)制到空閑區(qū)間,且嚴(yán)格按照內(nèi)存地址依次排列,與此同時(shí),GC線程將更新存活對(duì)象的內(nèi)存引用地址指向新的內(nèi)存地址。

此時(shí),空閑區(qū)間已經(jīng)與活動(dòng)區(qū)間交換,而垃圾對(duì)象現(xiàn)在已經(jīng)全部留在了原來的活動(dòng)區(qū)間,也就是現(xiàn)在的空閑區(qū)間。事實(shí)上,在活動(dòng)區(qū)間轉(zhuǎn)換為空間區(qū)間的同時(shí),垃圾對(duì)象已經(jīng)被一次性全部回收。

聽起來復(fù)雜嗎?

其實(shí)一點(diǎn)也不復(fù)雜,有了上一章的基礎(chǔ),相信各位理解這個(gè)算法不會(huì)費(fèi)太多力氣。LZ給各位繪制一幅圖來說明問題,如下所示。

其實(shí)這個(gè)圖依然是上一章的例子,只不過此時(shí)內(nèi)存被復(fù)制算法分成了兩部分,下面我們看下當(dāng)復(fù)制算法的GC線程處理之后,兩個(gè)區(qū)域會(huì)變成什么樣子,如下所示。

可以看到,1和4號(hào)對(duì)象被清除了,而2、3、5、6號(hào)對(duì)象則是規(guī)則的排列在剛才的空閑區(qū)間,也就是現(xiàn)在的活動(dòng)區(qū)間之內(nèi)。此時(shí)左半部分已經(jīng)變成了空閑區(qū)間,不難想象,在下一次GC之后,左邊將會(huì)再次變成活動(dòng)區(qū)間。

很明顯,復(fù)制算法彌補(bǔ)了標(biāo)記/清除算法中,內(nèi)存布局混亂的缺點(diǎn)。不過與此同時(shí),它的缺點(diǎn)也是相當(dāng)明顯的。

1、它浪費(fèi)了一半的內(nèi)存,這太要命了。

2、如果對(duì)象的存活率很高,我們可以極端一點(diǎn),假設(shè)是100%存活,那么我們需要將所有對(duì)象都復(fù)制一遍,并將所有引用地址重置一遍。復(fù)制這一工作所花費(fèi)的時(shí)間,在對(duì)象存活率達(dá)到一定程度時(shí),將會(huì)變的不可忽視

所以從以上描述不難看出,復(fù)制算法要想使用,最起碼對(duì)象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內(nèi)存的浪費(fèi)。

算法總結(jié)

這里L(fēng)Z給各位總結(jié)一下三個(gè)算法的共同點(diǎn)以及它們各自的優(yōu)勢(shì)劣勢(shì),讓各位對(duì)比一下,想必會(huì)更加清晰。

它們的共同點(diǎn)主要有以下兩點(diǎn)。

1、三個(gè)算法都基于根搜索算法去判斷一個(gè)對(duì)象是否應(yīng)該被回收,而支撐根搜索算法可以正常工作的理論依據(jù),就是語法中變量作用域的相關(guān)內(nèi)容。因此,要想防止內(nèi)存泄露,最根本的辦法就是掌握好變量作用域,而不應(yīng)該使用前面內(nèi)存管理雜談一章中所提到的C/C++式內(nèi)存管理方式。

2、在GC線程開啟時(shí),或者說GC過程開始時(shí),它們都要暫停應(yīng)用程序(stop the world)。

它們的區(qū)別LZ按照下面幾點(diǎn)來給各位展示。(>表示前者要優(yōu)于后者,=表示兩者效果一樣)

效率:復(fù)制算法>標(biāo)記/整理算法>標(biāo)記/清除算法(此處的效率只是簡(jiǎn)單的對(duì)比時(shí)間復(fù)雜度,實(shí)際情況不一定如此)。

內(nèi)存整齊度:復(fù)制算法=標(biāo)記/整理算法>標(biāo)記/清除算法。

內(nèi)存利用率:標(biāo)記/整理算法=標(biāo)記/清除算法>復(fù)制算法。

可以看到標(biāo)記/清除算法是比較落后的算法了,但是后兩種算法卻是在此基礎(chǔ)上建立的,俗話說“吃水不忘挖井人”,因此各位也莫要忘記了標(biāo)記/清除這一算法前輩。而且,在某些時(shí)候,標(biāo)記/清除也會(huì)有用武之地。

5、分代搜集算法

stop-and-copy垃圾收集器的一個(gè)缺陷是收集器必須復(fù)制所有的活動(dòng)對(duì)象,這增加了程序等待時(shí)間,這是coping算法低效的原因。在程序設(shè)計(jì)中有這樣的規(guī)律:多數(shù)對(duì)象存在的時(shí)間比較短,少數(shù)的存在時(shí)間比較長(zhǎng)。因此,generation算法將堆分成兩個(gè)或多個(gè),每個(gè)子堆作為對(duì)象的一代 (generation)。由于多數(shù)對(duì)象存在的時(shí)間比較短,隨著程序丟棄不使用的對(duì)象,垃圾收集器將從最年輕的子堆中收集這些對(duì)象。在分代式的垃圾收集器運(yùn)行后,上次運(yùn)行存活下來的對(duì)象移到下一最高代的子堆中,由于老一代的子堆不會(huì)經(jīng)常被回收,因而節(jié)省了時(shí)間。

對(duì)象分類

分代搜集算法是針對(duì)對(duì)象的不同特性,而使用適合的算法,這里面并沒有實(shí)際上的新算法產(chǎn)生。與其說分代搜集算法是第四個(gè)算法,不如說它是對(duì)前三個(gè)算法的實(shí)際應(yīng)用。

首先我們來探討一下對(duì)象的不同特性,接下來LZ和各位來一起給這些對(duì)象選擇GC算法。

內(nèi)存中的對(duì)象按照生命周期的長(zhǎng)短大致可以分為三種,以下命名均為L(zhǎng)Z個(gè)人的命名。

1、夭折對(duì)象:朝生夕滅的對(duì)象,通俗點(diǎn)講就是活不了多久就得死的對(duì)象。

例子:某一個(gè)方法的局域變量、循環(huán)內(nèi)的臨時(shí)變量等等。

2、老不死對(duì)象:這類對(duì)象一般活的比較久,歲數(shù)很大還不死,但歸根結(jié)底,老不死對(duì)象也幾乎早晚要死的,但也只是幾乎而已。

例子:緩存對(duì)象、數(shù)據(jù)庫連接對(duì)象、單例對(duì)象(單例模式)等等。

3、不滅對(duì)象:此類對(duì)象一般一旦出生就幾乎不死了,它們幾乎會(huì)一直永生不滅,記得,只是幾乎不滅而已。

例子:String池中的對(duì)象(享元模式)、加載過的類信息等等。

對(duì)象對(duì)應(yīng)的內(nèi)存區(qū)域

還記得前面介紹內(nèi)存管理時(shí),JVM對(duì)內(nèi)存的劃分嗎?

我們將上面三種對(duì)象對(duì)應(yīng)到內(nèi)存區(qū)域當(dāng)中,就是夭折對(duì)象和老不死對(duì)象都在JAVA堆,而不滅對(duì)象在方法區(qū)。

之前的一章中我們就已經(jīng)說過,對(duì)于JAVA堆,JVM規(guī)范要求必須實(shí)現(xiàn)GC,因而對(duì)于夭折對(duì)象和老不死對(duì)象來說,死幾乎是必然的結(jié)局,但也只是幾乎,還是難免會(huì)有一些對(duì)象會(huì)一直存活到應(yīng)用結(jié)束。然而JVM規(guī)范對(duì)方法區(qū)的GC并不做要求,所以假設(shè)一個(gè)JVM實(shí)現(xiàn)沒有對(duì)方法區(qū)實(shí)現(xiàn)GC,那么不滅對(duì)象就是真的不滅對(duì)象了。

由于不滅對(duì)象的生命周期過長(zhǎng),因此分代搜集算法就是針對(duì)的JAVA堆而設(shè)計(jì)的,也就是針對(duì)夭折對(duì)象和老不死對(duì)象

JAVA堆的對(duì)象回收(夭折對(duì)象和老不死對(duì)象)

有了以上分析,我們來看看分代搜集算法如何處理JAVA堆的內(nèi)存回收的,也就是夭折對(duì)象與老不死對(duì)象的回收。

夭折對(duì)象:這類對(duì)象朝生夕滅,存活時(shí)間短,還記得復(fù)制算法的使用要求嗎?那就是對(duì)象存活率不能太高,因此夭折對(duì)象是最適合使用復(fù)制算法的

小疑問:50%內(nèi)存的浪費(fèi)怎么辦?

答疑:因?yàn)樨舱蹖?duì)象一般存活率較低,因此可以不使用50%的內(nèi)存作為空閑,一般的,使用兩塊10%的內(nèi)存作為空閑和活動(dòng)區(qū)間,而另外80%的內(nèi)存,則是用來給新建對(duì)象分配內(nèi)存的。一旦發(fā)生GC,將10%的活動(dòng)區(qū)間與另外80%中存活的對(duì)象轉(zhuǎn)移到10%的空閑區(qū)間,接下來,將之前90%的內(nèi)存全部釋放,以此類推。

為了讓各位更加清楚的看出來這個(gè)GC流程,LZ給出下面圖示。

圖中標(biāo)注了三個(gè)區(qū)域中在各個(gè)階段,各自內(nèi)存的情況。相信看著圖,它的GC流程已經(jīng)不難理解了。

不過有兩點(diǎn)LZ需要提一下,第一點(diǎn)是使用這樣的方式,我們只浪費(fèi)了10%的內(nèi)存,這個(gè)是可以接受的,因?yàn)槲覀儞Q來了內(nèi)存的整齊排列與GC速度。第二點(diǎn)是,這個(gè)策略的前提是,每次存活的對(duì)象占用的內(nèi)存不能超過這10%的大小,一旦超過,多出的對(duì)象將無法復(fù)制

為了解決上面的意外情況,也就是存活對(duì)象占用的內(nèi)存太大時(shí)的情況,高手們將JAVA堆分成兩部分來處理,上述三個(gè)區(qū)域則是第一部分,稱為新生代或者年輕代。而余下的一部分,專門存放老不死對(duì)象的則稱為年老代

是不是很貼切的名字呢?下面我們看看老不死對(duì)象的處理方式。

老不死對(duì)象:這一類對(duì)象存活率非常高,因?yàn)樗鼈兇蠖嗍菑男律D(zhuǎn)過來的。就像人一樣,活的年月久了,就變成老不死了。

通常情況下,以下兩種情況發(fā)生的時(shí)候,對(duì)象會(huì)從新生代區(qū)域轉(zhuǎn)到年老帶區(qū)域。

1、在新生代里的每一個(gè)對(duì)象,都會(huì)有一個(gè)年齡,當(dāng)這些對(duì)象的年齡到達(dá)一定程度時(shí)(年齡就是熬過的GC次數(shù),每次GC如果對(duì)象存活下來,則年齡加1),則會(huì)被轉(zhuǎn)到年老代,而這個(gè)轉(zhuǎn)入年老代的年齡值,一般在JVM中是可以設(shè)置的。

2、在新生代存活對(duì)象占用的內(nèi)存超過10%時(shí),則多余的對(duì)象會(huì)放入年老代。這種時(shí)候,年老代就是新生代的“備用倉(cāng)庫”。

針對(duì)老不死對(duì)象的特性,顯然不再適合使用復(fù)制算法,因?yàn)樗拇婊盥侍?,而且不要忘了,如果年老代再使用?fù)制算法,它可是沒有備用倉(cāng)庫的。因此一般針對(duì)老不死對(duì)象只能采用標(biāo)記/整理或者標(biāo)記/清除算法。

方法區(qū)的對(duì)象回收(不滅對(duì)象)

以上兩種情況已經(jīng)解決了GC的大部分問題,因?yàn)镴AVA堆是GC的主要關(guān)注對(duì)象,而以上也已經(jīng)包含了分代搜集算法的全部?jī)?nèi)容,接下來對(duì)于不滅對(duì)象的回收,已經(jīng)不屬于分代搜集算法的內(nèi)容。

不滅對(duì)象存在于方法區(qū),在我們常用的hotspot虛擬機(jī)(JDK默認(rèn)的JVM)中,方法區(qū)也被親切的稱為永久代,又是一個(gè)很貼切的名字不是嗎?

其實(shí)在很久很久以前,是不存在永久代的。當(dāng)時(shí)永久代與年老代都存放在一起,里面包含了JAVA類的實(shí)例信息以及類信息。但是后來發(fā)現(xiàn),對(duì)于類信息的卸載幾乎很少發(fā)生,因此便將二者分離開來。幸運(yùn)的是,這樣做確實(shí)提高了不少性能。于是永久代便被拆分出來了。

這一部分區(qū)域的GC與年老代采用相似的方法,由于都沒有“備用倉(cāng)庫”,二者都是只能使用標(biāo)記/清除和標(biāo)記/整理算法。

回收的時(shí)機(jī)

JVM在進(jìn)行GC時(shí),并非每次都對(duì)上面三個(gè)內(nèi)存區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC?or Full GC),它們所針對(duì)的區(qū)域如下。

普通GC(minor GC):只針對(duì)新生代區(qū)域的GC。

全局GC(major GC or Full GC):針對(duì)年老代的GC,偶爾伴隨對(duì)新生代的GC以及對(duì)永久代的GC。

由于年老代與永久代相對(duì)來說GC效果不好,而且二者的內(nèi)存使用增長(zhǎng)速度也慢,因此一般情況下,需要經(jīng)過好幾次普通GC,才會(huì)觸發(fā)一次全局GC。

三、垃圾收集器:

通俗的講,使用編程語言將算法實(shí)現(xiàn)出來,產(chǎn)生的程序就是垃圾搜集器了。既然談到了編程語言的實(shí)現(xiàn),那么在討論垃圾搜集器的時(shí)候,就已經(jīng)涉及到具體的虛擬機(jī)實(shí)現(xiàn)了。

或許有不少做JAVA開發(fā)的猿友還不知道,我們平時(shí)使用的JDK中,默認(rèn)的JVM是hotspot,換句話說,我們大部分時(shí)候使用的JVM都是hotspot的實(shí)現(xiàn)版本,因此,本次LZ討論垃圾搜集器都是基于hotspot版JVM來進(jìn)行的,請(qǐng)各位猿友要知曉這一點(diǎn)。

更直觀的,我們可以在我們平時(shí)開發(fā)的機(jī)子上,輸入java -version來查看JVM的版本,相信大部分猿友對(duì)這個(gè)命令都不陌生吧,LZ的機(jī)子截圖如下。

垃圾搜集器的分類

上面我們已經(jīng)提到,垃圾搜集器實(shí)際就是算法的編程語言實(shí)現(xiàn)。既然牽扯到編程語言,那么必然離不開線程,而且我們?cè)谇懊嬷v解算法的時(shí)候也一直假設(shè)是一條GC線程在做著GC的事情。

因此,垃圾搜集器大致分為以下三類。

串行搜集器(serial collector):它只有一條GC線程,且就像前面說的,它在運(yùn)行的時(shí)候需要暫停用戶程序(stop the world)。

并行搜集器(parallel collector):它有多條GC線程,且它也需要暫停用戶程序(stop the world)。

并發(fā)搜集器(concurrent collector):它有一條或多條GC線程,且它需要在部分階段暫停用戶程序(stop the world),部分階段與用戶程序并發(fā)執(zhí)行。

并發(fā)(concurrent)與并行(parallel)

看完上面的定義,相信有一部分猿友已經(jīng)蒙了,一會(huì)單線程,一會(huì)多線程,一會(huì)串行,一會(huì)并行,一會(huì)并發(fā),這都神馬玩意?

單線程和多線程就不必多說了,這個(gè)很好理解,串行與并行也比較好理解,難于分辨的就是并行(parallel)與并發(fā)(concurrent)。

對(duì)于很多有關(guān)并發(fā)的解釋,LZ覺得有一個(gè)最貼切。它是這么解釋的,并發(fā)就是兩個(gè)任務(wù)A和B需要相互獨(dú)立的運(yùn)行,并且A任務(wù)先開始后,B任務(wù)在A任務(wù)結(jié)束之前開始了。

并發(fā)本身是比較好理解的,那么它與并行的關(guān)系與區(qū)別是什么呢?

事實(shí)上,并行是并發(fā)的一種實(shí)現(xiàn)方式。LZ覺得這么說各位可能會(huì)更好理解,當(dāng)然,并行并不是并發(fā)的唯一實(shí)現(xiàn)方式,還有一種就是我們所熟悉的時(shí)間片切換。也就是A任務(wù)執(zhí)行一會(huì),B任務(wù)執(zhí)行一會(huì),交替執(zhí)行。

并行必須在多核多處理器或者分布式系統(tǒng)(本質(zhì)還是多核多處理器)的前提下才能發(fā)生,而交替執(zhí)行或者說時(shí)間片切換是在單核的處理器上發(fā)生的。

hotspot中的垃圾搜集器

我們上面已經(jīng)簡(jiǎn)單探討了垃圾搜集器的分類,在hotspotJVM中,每一個(gè)種類的垃圾搜集器都有對(duì)應(yīng)的實(shí)現(xiàn),如下。

串行搜集器的實(shí)現(xiàn):serial(用于新生代,采用復(fù)制算法)、serial old(用于年老代,采用標(biāo)記/整理算法)

并行搜集器的實(shí)現(xiàn):ParNew(用于新生代,采用復(fù)制算法)、Parallel Scavenge(用于新生代,采用復(fù)制算法)、Parallel old(用于年老代,采用標(biāo)記/整理算法)

并發(fā)搜集器的實(shí)現(xiàn):concurrent mark sweep[CMS](用于年老代,采用標(biāo)記/清除算法)

可以看到,上面每一種垃圾搜集器都是針對(duì)不同內(nèi)存區(qū)域所設(shè)計(jì)的,因?yàn)樗鼈儾捎玫乃惴ú煌?,凡是用于新生代的都是使用的?fù)制算法,而用于年老代的都是使用的標(biāo)記/清除或者標(biāo)記/整理算法。

在實(shí)際應(yīng)用中,我們需要給JVM的新生代和年老代分別選擇垃圾搜集器,可以看到無論是新生代還是年老代都分別有三種實(shí)現(xiàn),換句話說,我們應(yīng)該有3*3=9種選擇。但是,事實(shí)并非如此。

事實(shí)上,這六種垃圾搜集器只有六種選擇,因?yàn)橛械睦鸭饔捎诰唧w實(shí)現(xiàn)的方式等一系列原因無法在一起工作,如下圖。

針對(duì)上圖,紅的就是串行搜集器,綠的是并行搜集器,唯一一個(gè)黃的是并發(fā)搜集器。上面三個(gè)是新生代的搜集器,下面三個(gè)是年老代的搜集器。兩者之間有連線,則表示兩者可以配合工作。

這六種組合并沒有說哪個(gè)組合最強(qiáng),哪個(gè)組合最弱,還是那句話,只有最合適的,沒有最好的。因此這就需要我們對(duì)每一種組合有一定的認(rèn)識(shí),才能在使用的時(shí)候選擇更適合的垃圾搜集器

關(guān)于垃圾搜集器:JVM內(nèi)存管理------垃圾搜集器精解(讓你在垃圾搜集器的世界里耍的游刃有余) - 左瀟龍 - 博客園、JVM內(nèi)存管理------垃圾搜集器參數(shù)精解 - 左瀟龍 - 博客園

五、finalize()方法

在JVM垃圾回收器收集一個(gè)對(duì)象之前,一般要求程序調(diào)用適當(dāng)?shù)姆椒ㄡ尫刨Y源,但在沒有明確釋放資源的情況下,Java提供了缺省機(jī)制來終止該對(duì)象心釋放資源,這個(gè)方法就是finalize()。它的原型為:

protected void finalize() throws Throwable

在finalize()方法返回之后,對(duì)象消失,垃圾收集開始執(zhí)行。原型中的throws Throwable表示它可以拋出任何類型的異常。

之所以要使用finalize(),是存在著垃圾回收器不能處理的特殊情況。假定你的對(duì)象(并非使用new方法)獲得了一塊“特殊”的內(nèi)存區(qū)域,由于垃圾回收器只知道那些顯示地經(jīng)由new分配的內(nèi)存空間,所以它不知道該如何釋放這塊“特殊”的內(nèi)存區(qū)域,那么這個(gè)時(shí)候java允許在類中定義一個(gè)由finalize()方法。

特殊的區(qū)域例如:1)由于在分配內(nèi)存的時(shí)候可能采用了類似 C語言的做法,而非JAVA的通常new做法。這種情況主要發(fā)生在native method中,比如native method調(diào)用了C/C++方法malloc()函數(shù)系列來分配存儲(chǔ)空間,但是除非調(diào)用free()函數(shù),否則這些內(nèi)存空間將不會(huì)得到釋放,那么這個(gè)時(shí)候就可能造成內(nèi)存泄漏。但是由于free()方法是在C/C++中的函數(shù),所以finalize()中可以用本地方法來調(diào)用它。以釋放這些“特殊”的內(nèi)存空間。2)又或者打開的文件資源,這些資源不屬于垃圾回收器的回收范圍。

換言之,finalize()的主要用途是釋放一些其他做法開辟的內(nèi)存空間,以及做一些清理工作。因?yàn)樵贘AVA中并沒有提夠像“析構(gòu)”函數(shù)或者類似概念的函數(shù),要做一些類似清理工作的時(shí)候,必須自己動(dòng)手創(chuàng)建一個(gè)執(zhí)行清理工作的普通方法,也就是override Object這個(gè)類中的finalize()方法。例如,假設(shè)某一個(gè)對(duì)象在創(chuàng)建過程中會(huì)將自己繪制到屏幕上,如果不是明確地從屏幕上將其擦出,它可能永遠(yuǎn)都不會(huì)被清理。如果在finalize()加入某一種擦除功能,當(dāng)GC工作時(shí),finalize()得到了調(diào)用,圖像就會(huì)被擦除。要是GC沒有發(fā)生,那么這個(gè)圖像就會(huì)

被一直保存下來。

一旦垃圾回收器準(zhǔn)備好釋放對(duì)象占用的存儲(chǔ)空間,首先會(huì)去調(diào)用finalize()方法進(jìn)行一些必要的清理工作。只有到下一次再進(jìn)行垃圾回收動(dòng)作的時(shí)候,才會(huì)真正釋放這個(gè)對(duì)象所占用的內(nèi)存空間。

在普通的清除工作中,為清除一個(gè)對(duì)象,那個(gè)對(duì)象的用戶必須在希望進(jìn)行清除的地點(diǎn)調(diào)用一個(gè)清除方法。這與C++"析構(gòu)函數(shù)"的概念稍有抵觸。在C++中,所有對(duì)象都會(huì)破壞(清除)?;蛘邠Q句話說,所有對(duì)象都"應(yīng)該"破壞。若將C++對(duì)象創(chuàng)建成一個(gè)本地對(duì)象,比如在堆棧中創(chuàng)建(在Java中是不可能的,Java都在堆中),那么清除或破壞工作就會(huì)在"結(jié)束花括號(hào)"所代表的、創(chuàng)建這個(gè)對(duì)象的作用域的末尾進(jìn)行。若對(duì)象是用new創(chuàng)建的(類似于Java),那么當(dāng)程序員調(diào)用C++的 delete命令時(shí)(Java沒有這個(gè)命令),就會(huì)調(diào)用相應(yīng)的析構(gòu)函數(shù)。若程序員忘記了,那么永遠(yuǎn)不會(huì)調(diào)用析構(gòu)函數(shù),我們最終得到的將是一個(gè)內(nèi)存"漏洞",另外還包括對(duì)象的其他部分永遠(yuǎn)不會(huì)得到清除。

相反,Java不允許我們創(chuàng)建本地(局部)對(duì)象--無論如何都要使用new。但在Java中,沒有"delete"命令來釋放對(duì)象,因?yàn)槔厥掌鲿?huì)幫助我們自動(dòng)釋放存儲(chǔ)空間。所以如果站在比較簡(jiǎn)化的立場(chǎng),我們可以說正是由于存在垃圾回收機(jī)制,所以Java沒有析構(gòu)函數(shù)。然而,隨著以后學(xué)習(xí)的深入,就會(huì)知道垃圾收集器的存在并不能完全消除對(duì)析構(gòu)函數(shù)的需要,或者說不能消除對(duì)析構(gòu)函數(shù)代表的那種機(jī)制的需要(原因見下一段。另外finalize()函數(shù)是在垃圾回收器準(zhǔn)備釋放對(duì)象占用的存儲(chǔ)空間的時(shí)候被調(diào)用的,絕對(duì)不能直接調(diào)用finalize(),所以應(yīng)盡量避免用它)。若希望執(zhí)行除釋放存儲(chǔ)空間之外的其他某種形式的清除工作,仍然必須調(diào)用Java中的一個(gè)方法。它等價(jià)于C++的析構(gòu)函數(shù),只是沒后者方便。

在C++中所有的對(duì)象運(yùn)用delete()一定會(huì)被銷毀,而JAVA里的對(duì)象并非總會(huì)被垃圾回收器回收。In another word, 1 對(duì)象可能不被垃圾回收,2 垃圾回收并不等于“析構(gòu)”,3 垃圾回收只與內(nèi)存有關(guān)。也就是說,并不是如果一個(gè)對(duì)象不再被使用,是不是要在finalize()中釋放這個(gè)對(duì)象中含有的其它對(duì)象呢?不是的。因?yàn)闊o論對(duì)象是如何創(chuàng)建的,垃圾回收器都會(huì)負(fù)責(zé)釋放那些對(duì)象占有的內(nèi)存。

六、觸發(fā)主GC(Garbage Collector)的條件

JVM進(jìn)行次GC的頻率很高,但因?yàn)檫@種GC占用時(shí)間極短,所以對(duì)系統(tǒng)產(chǎn)生的影響不大。更值得關(guān)注的是主GC的觸發(fā)條件,因?yàn)樗鼘?duì)系統(tǒng)影響很明顯??偟膩碚f,有兩個(gè)條件會(huì)觸發(fā)主GC:

1)當(dāng)應(yīng)用程序空閑時(shí),即沒有應(yīng)用線程在運(yùn)行時(shí),GC會(huì)被調(diào)用。因?yàn)镚C在優(yōu)先級(jí)最低的線程中進(jìn)行,所以當(dāng)應(yīng)用忙時(shí),GC線程就不會(huì)被調(diào)用,但以下條件除外。

2)Java堆內(nèi)存不足時(shí),GC會(huì)被調(diào)用。當(dāng)應(yīng)用線程在運(yùn)行,并在運(yùn)行過程中創(chuàng)建新對(duì)象,若這時(shí)內(nèi)存空間不足,JVM就會(huì)強(qiáng)制地調(diào)用GC線程,以便回收內(nèi)存用于新的分配。若GC一次之后仍不能滿足內(nèi)存分配的要求,JVM會(huì)再進(jìn)行兩次GC作進(jìn)一步的嘗試,若仍無法滿足要求,則 JVM將報(bào)“out of memory”的錯(cuò)誤,Java應(yīng)用將停止。

由于是否進(jìn)行主GC由JVM根據(jù)系統(tǒng)環(huán)境決定,而系統(tǒng)環(huán)境在不斷的變化當(dāng)中,所以主GC的運(yùn)行具有不確定性,無法預(yù)計(jì)它何時(shí)必然出現(xiàn),但可以確定的是對(duì)一個(gè)長(zhǎng)期運(yùn)行的應(yīng)用來說,其主GC是反復(fù)進(jìn)行的。

七、減少GC開銷的措施

根據(jù)上述GC的機(jī)制,程序的運(yùn)行會(huì)直接影響系統(tǒng)環(huán)境的變化,從而影響GC的觸發(fā)。若不針對(duì)GC的特點(diǎn)進(jìn)行設(shè)計(jì)和編碼,就會(huì)出現(xiàn)內(nèi)存駐留等一系列負(fù)面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個(gè)方面:

(1)不要顯式調(diào)用System.gc()

此函數(shù)建議JVM進(jìn)行主GC,雖然只是建議而非一定,但很多情況下它會(huì)觸發(fā)主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數(shù)。

(2)盡量減少臨時(shí)對(duì)象的使用

臨時(shí)對(duì)象在跳出函數(shù)調(diào)用后,會(huì)成為垃圾,少用臨時(shí)變量就相當(dāng)于減少了垃圾的產(chǎn)生,從而延長(zhǎng)了出現(xiàn)上述第二個(gè)觸發(fā)條件出現(xiàn)的時(shí)間,減少了主GC的機(jī)會(huì)。

(3)對(duì)象不用時(shí)最好顯式置為Null

一般而言,為Null的對(duì)象都會(huì)被作為垃圾處理,所以將不用的對(duì)象顯式地設(shè)為Null,有利于GC收集器判定垃圾,從而提高了GC的效率。

(4)盡量使用StringBuffer,而不用String來累加字符串

由于String是固定長(zhǎng)的字符串對(duì)象,累加String對(duì)象時(shí),并非在一個(gè)String對(duì)象中擴(kuò)增,而是重新創(chuàng)建新的String對(duì)象,如Str5=Str1+Str2+Str3+Str4,這條語句執(zhí)行過程中會(huì)產(chǎn)生多個(gè)垃圾對(duì)象,因?yàn)閷?duì)次作“+”操作時(shí)都必須創(chuàng)建新的String對(duì)象,但這些過渡對(duì)象對(duì)系統(tǒng)來說是沒有實(shí)際意義的,只會(huì)增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長(zhǎng)的,它在原有基礎(chǔ)上進(jìn)行擴(kuò)增,不會(huì)產(chǎn)生中間對(duì)象。

(5)能用基本類型如Int,Long,就不用Integer,Long對(duì)象

基本類型變量占用的內(nèi)存資源比相應(yīng)對(duì)象占用的少得多,如果沒有必要,最好使用基本變量。

(6)盡量少用靜態(tài)對(duì)象變量

靜態(tài)變量屬于全局變量,不會(huì)被GC回收,它們會(huì)一直占用內(nèi)存。

(7)分散對(duì)象創(chuàng)建或刪除的時(shí)間

集中在短時(shí)間內(nèi)大量創(chuàng)建新對(duì)象,特別是大對(duì)象,會(huì)導(dǎo)致突然需要大量?jī)?nèi)存,JVM在面臨這種情況時(shí),只能進(jìn)行主GC,以回收內(nèi)存或整合內(nèi)存碎片,從而增加主GC的頻率。集中刪除對(duì)象,道理也是一樣的。它使得突然出現(xiàn)了大量的垃圾對(duì)象,空閑空間必然減少,從而大大增加了下一次創(chuàng)建新對(duì)象時(shí)強(qiáng)制主GC的機(jī)會(huì)。

八、關(guān)于垃圾回收的幾點(diǎn)補(bǔ)充

經(jīng)過上述的說明,可以發(fā)現(xiàn)垃圾回收有以下的幾個(gè)特點(diǎn):

(1)垃圾收集發(fā)生的不可預(yù)知性:由于實(shí)現(xiàn)了不同的垃圾回收算法和采用了不同的收集機(jī)制,所以它有可能是定時(shí)發(fā)生,有可能是當(dāng)出現(xiàn)系統(tǒng)空閑CPU資源時(shí)發(fā)生,也有可能是和原始的垃圾收集一樣,等到內(nèi)存消耗出現(xiàn)極限時(shí)發(fā)生,這與垃圾收集器的選擇和具體的設(shè)置都有關(guān)系。

(2)垃圾收集的精確性:主要包括2 個(gè)方面:(a)垃圾收集器能夠精確標(biāo)記活著的對(duì)象;(b)垃圾收集器能夠精確地定位對(duì)象之間的引用關(guān)系。前者是完全地回收所有廢棄對(duì)象的前提,否則就可能造成內(nèi)存泄漏。而后者則是實(shí)現(xiàn)歸并和復(fù)制等算法的必要條件。所有不可達(dá)對(duì)象都能夠可靠地得到回收,所有對(duì)象都能夠重新分配,允許對(duì)象的復(fù)制和對(duì)象內(nèi)存的縮并,這樣就有效地防止內(nèi)存的支離破碎。

(3)現(xiàn)在有許多種不同的垃圾收集器,每種有其算法且其表現(xiàn)各異,既有當(dāng)垃圾收集開始時(shí)就停止應(yīng)用程序的運(yùn)行,又有當(dāng)垃圾收集開始時(shí)也允許應(yīng)用程序的線程運(yùn)行,還有在同一時(shí)間垃圾收集多線程運(yùn)行。

(4)垃圾收集的實(shí)現(xiàn)和具體的JVM 以及JVM的內(nèi)存模型有非常緊密的關(guān)系。不同的JVM 可能采用不同的垃圾收集,而JVM 的內(nèi)存模型決定著該JVM可以采用哪些類型垃圾收集?,F(xiàn)在,HotSpot 系列JVM中的內(nèi)存系統(tǒng)都采用先進(jìn)的面向?qū)ο蟮目蚣茉O(shè)計(jì),這使得該系列JVM都可以采用最先進(jìn)的垃圾收集。

(5)隨著技術(shù)的發(fā)展,現(xiàn)代垃圾收集技術(shù)提供許多可選的垃圾收集器,而且在配置每種收集器的時(shí)候又可以設(shè)置不同的參數(shù),這就使得根據(jù)不同的應(yīng)用環(huán)境獲得最優(yōu)的應(yīng)用性能成為可能。

針對(duì)以上特點(diǎn),我們?cè)谑褂玫臅r(shí)候要注意:

(1)不要試圖去假定垃圾收集發(fā)生的時(shí)間,這一切都是未知的。比如,方法中的一個(gè)臨時(shí)對(duì)象在方法調(diào)用完畢后就變成了無用對(duì)象,這個(gè)時(shí)候它的內(nèi)存就可以被釋放。

(2)Java中提供了一些和垃圾收集打交道的類,而且提供了一種強(qiáng)行執(zhí)行垃圾收集的方法--調(diào)用System.gc(),但這同樣是個(gè)不確定的方法。Java 中并不保證每次調(diào)用該方法就一定能夠啟動(dòng)垃圾收集,它只不過會(huì)向JVM發(fā)出這樣一個(gè)申請(qǐng),到底是否真正執(zhí)行垃圾收集,一切都是個(gè)未知數(shù)。

(3)挑選適合自己的垃圾收集器。一般來說,如果系統(tǒng)沒有特殊和苛刻的性能要求,可以采用JVM的缺省選項(xiàng)。否則可以考慮使用有針對(duì)性的垃圾收集器,比如增量收集器就比較適合實(shí)時(shí)性要求較高的系統(tǒng)之中。系統(tǒng)具有較高的配置,有比較多的閑置資源,可以考慮使用并行標(biāo)記/清除收集器。

(4)關(guān)鍵的也是難把握的問題是內(nèi)存泄漏。良好的編程習(xí)慣和嚴(yán)謹(jǐn)?shù)木幊虘B(tài)度永遠(yuǎn)是最重要的,不要讓自己的一個(gè)小錯(cuò)誤導(dǎo)致內(nèi)存出現(xiàn)大漏洞。

(5)盡早釋放無用對(duì)象的引用。大多數(shù)程序員在使用臨時(shí)變量的時(shí)候,都是讓引用變量在退出活動(dòng)域(scope)后,自動(dòng)設(shè)置為null,暗示垃圾收集器來收集該對(duì)象,還必須注意該引用的對(duì)象是否被監(jiān)聽,如果有,則要去掉監(jiān)聽器,然后再賦空值。

參考:

JVM內(nèi)存管理------GC簡(jiǎn)介 - 左瀟龍 - 博客園

?Java垃圾回收機(jī)制 - zsuguangh的專欄 - 博客頻道 - CSDN.NET

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡(jiǎn)欲明心閱讀 90,388評(píng)論 17 311
  • 原文閱讀 前言 這段時(shí)間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號(hào)寫博客了,挺好的~給他們點(diǎn)贊,這博客我...
    碼農(nóng)戲碼閱讀 6,163評(píng)論 2 31
  • 作者:一字馬胡 轉(zhuǎn)載標(biāo)志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,334評(píng)論 0 7
  • JVM架構(gòu) 當(dāng)一個(gè)程序啟動(dòng)之前,它的class會(huì)被類裝載器裝入方法區(qū)(Permanent區(qū)),執(zhí)行引擎讀取方法區(qū)的...
    cocohaifang閱讀 1,852評(píng)論 0 7
  • 1.一些概念 1.1.數(shù)據(jù)類型 Java虛擬機(jī)中,數(shù)據(jù)類型可以分為兩類:基本類型和引用類型。基本類型的變量保存原始...
    落落落落大大方方閱讀 4,830評(píng)論 4 86

友情鏈接更多精彩內(nèi)容