Java內(nèi)存模型及GC原理

原文鏈接:https://www.cnblogs.com/byue/p/5734779.html

一個(gè)優(yōu)秀Java程序員,必須了解Java內(nèi)存模型、GC工作原理,以及如何優(yōu)化GC的性能、與GC進(jìn)行有限的交互,有一些應(yīng)用程序?qū)π阅芤筝^高,例如嵌入式系統(tǒng)、實(shí)時(shí)系統(tǒng)等,只有全面提升內(nèi)存的管理效率,才能提高整個(gè)應(yīng)用程序的性能。

本文將從JVM內(nèi)存模型、GC工作原理,以及GC的幾個(gè)關(guān)鍵問題進(jìn)行探討,從GC角度提高Java程序的性能。


一、Java內(nèi)存模型


按照官方的說法:Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配。

JVM主要管理兩種類型內(nèi)存:堆和非堆,堆內(nèi)存(Heap?Memory)是在 Java 虛擬機(jī)啟動時(shí)創(chuàng)建,非堆內(nèi)存(Non-heap Memory)是在JVM堆之外的內(nèi)存。

簡單來說,堆是Java代碼可及的內(nèi)存,留給開發(fā)人員使用的;非堆是JVM留給自己用的,包含方法區(qū)、JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如JITCompiler,Just-in-time Compiler,即時(shí)編譯后的代碼緩存)、每個(gè)類結(jié)構(gòu)(如運(yùn)行時(shí)常數(shù)池、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法的代碼。

JVM 內(nèi)存包含如下幾個(gè)部分:

堆內(nèi)存(Heap Memory): 存放Java對象

非堆內(nèi)存(Non-Heap Memory): 存放類加載信息和其它meta-data

其它(Other): 存放JVM 自身代碼等

在JVM啟動時(shí),就已經(jīng)保留了固定的內(nèi)存空間給Heap內(nèi)存,這部分內(nèi)存并不一定都會被JVM使用,但是可以確定的是這部分保留的內(nèi)存不會被其他進(jìn)程使用,這部分內(nèi)存大小由-Xmx?參數(shù)指定。而另一部分內(nèi)存在JVM啟動時(shí)就分配給JVM,作為JVM的初始Heap內(nèi)存使用,這部分內(nèi)存是由?-Xms?參數(shù)指定。


詳細(xì)配置文件目錄:eclipse/eclipse.ini



默認(rèn)空余堆內(nèi)存小于40%時(shí),JVM 就會增大堆直到-Xmx 的最大限制,可以由?-XX:MinHeapFreeRatio?指定。?

默認(rèn)空余堆內(nèi)存大于70%時(shí),JVM 會減少堆直到-Xms的最小限制,可以由?-XX:MaxHeapFreeRatio?指定,詳見

可以通過-XX:MaxPermSize?設(shè)置Non-Heap大小,詳細(xì)參見我的百度博客


二、Java內(nèi)存分配


Java的內(nèi)存管理實(shí)際上就是變量和對象的管理,其中包括對象的分配和釋放。

JVM內(nèi)存申請過程如下:

JVM 會試圖為相關(guān)Java對象在Eden中初始化一塊內(nèi)存區(qū)域

當(dāng)Eden空間足夠時(shí),內(nèi)存申請結(jié)束;否則到下一步

JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收),釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區(qū)

Survivor區(qū)被用來作為Eden及OLD的中間交換區(qū)域,當(dāng)OLD區(qū)空間足夠時(shí),Survivor區(qū)的對象會被移到Old區(qū),否則會被保留在Survivor區(qū)

當(dāng)OLD區(qū)空間不夠時(shí),JVM 會在OLD區(qū)進(jìn)行完全的垃圾收集(0級)

完全垃圾收集后,若Survivor及OLD區(qū)仍然無法存放從Eden復(fù)制過來的部分對象,導(dǎo)致JVM無法在Eden區(qū)為新對象創(chuàng)建內(nèi)存區(qū)域,則出現(xiàn)”out of memory”錯(cuò)誤


三、GC基本原理

GC(Garbage Collection),是JAVA/.NET中的垃圾收集器。

Java是由C++發(fā)展來的,它擯棄了C++中一些繁瑣容易出錯(cuò)的東西,引入了計(jì)數(shù)器的概念,其中有一條就是這個(gè)GC機(jī)制(C#借鑒了JAVA)

編程人員容易出現(xiàn)問題的地方,忘記或者錯(cuò)誤的內(nèi)存回收會導(dǎo)致程序或系統(tǒng)的不穩(wěn)定甚至崩潰,Java提供的GC功能可以自動監(jiān)測對象是否超過作用域從而達(dá)到自動回收內(nèi)存的目的,Java語言沒有提供釋放已分配內(nèi)存的顯示操作方法。所以,Java的內(nèi)存管理實(shí)際上就是對象的管理,其中包括對象的分配和釋放。

對于程序員來說,分配對象使用new關(guān)鍵字;釋放對象時(shí),只要將對象所有引用賦值為null,讓程序不能夠再訪問到這個(gè)對象,我們稱該對象為"不可達(dá)的".GC將負(fù)責(zé)回收所有"不可達(dá)"對象的內(nèi)存空間。

對于GC來說,當(dāng)程序員創(chuàng)建對象時(shí),GC就開始監(jiān)控這個(gè)對象的地址、大小以及使用情況。通常,GC采用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是"可達(dá)的",哪些對象是"不可達(dá)的".當(dāng)GC確定一些對象為"不可達(dá)"時(shí),GC就有責(zé)任回收這些內(nèi)存空間。但是,為了保證 GC能夠在不同平臺實(shí)現(xiàn)的問題,Java規(guī)范對GC的很多行為都沒有進(jìn)行嚴(yán)格的規(guī)定。例如,對于采用什么類型的回收算法、什么時(shí)候進(jìn)行回收等重要問題都沒有明確的規(guī)定。因此,不同的JVM的實(shí)現(xiàn)者往往有不同的實(shí)現(xiàn)算法。這也給Java程序員的開發(fā)帶來行多不確定性。本文研究了幾個(gè)與GC工作相關(guān)的問題,努力減少這種不確定性給Java程序帶來的負(fù)面影響。


四、GC分代劃分


JVM內(nèi)存模型中Heap區(qū)分兩大塊,一塊是 Young Generation,另一塊是Old Generation

1) 在Young?Generation中,有一個(gè)叫Eden Space的空間,主要是用來存放新生的對象,還有兩個(gè)Survivor Spaces(from、to),它們的大小總是一樣,它們用來存放每次垃圾回收后存活下來的對象。

2) 在Old Generation中,主要存放應(yīng)用程序中生命周期長的內(nèi)存對象。

3) 在Young?Generation塊中,垃圾回收一般用Copying的算法,速度快。每次GC的時(shí)候,存活下來的對象首先由Eden拷貝到某個(gè)SurvivorSpace,當(dāng)Survivor Space空間滿了后,剩下的live對象就被直接拷貝到OldGeneration中去。因此,每次GC后,Eden內(nèi)存塊會被清空。

4) 在Old Generation塊中,垃圾回收一般用mark-compact的算法,速度慢些,但減少內(nèi)存要求。

5) 垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收Young中的垃圾,內(nèi)存溢出通常發(fā)生于OLD段或Perm段垃圾回收后,仍然無內(nèi)存空間容納新的Java對象的情況。


五、增量式GC

增量式GC(Incremental GC),是GC在JVM中通常是由一個(gè)或一組進(jìn)程來實(shí)現(xiàn)的,它本身也和用戶程序一樣占用heap空間,運(yùn)行時(shí)也占用CPU。

當(dāng)GC進(jìn)程運(yùn)行時(shí),應(yīng)用程序停止運(yùn)行。因此,當(dāng)GC運(yùn)行時(shí)間較長時(shí),用戶能夠感到Java程序的停頓,另外一方面,如果GC運(yùn)行時(shí)間太短,則可能對象回收率太低,這意味著還有很多應(yīng)該回收的對象沒有被回收,仍然占用大量內(nèi)存。因此,在設(shè)計(jì)GC的時(shí)候,就必須在停頓時(shí)間和回收率之間進(jìn)行權(quán)衡。一個(gè)好的GC實(shí)現(xiàn)允許用戶定義自己所需要的設(shè)置,例如有些內(nèi)存有限的設(shè)備,對內(nèi)存的使用量非常敏感,希望GC能夠準(zhǔn)確的回收內(nèi)存,它并不在意程序速度的快慢。另外一些實(shí)時(shí)網(wǎng)絡(luò)游戲,就不能夠允許程序有長時(shí)間的中斷。

增量式GC就是通過一定的回收算法,把一個(gè)長時(shí)間的中斷,劃分為很多個(gè)小的中斷,通過這種方式減少GC對用戶程序的影響。雖然,增量式GC在整體性能上可能不如普通GC的效率高,但是它能夠減少程序的最長停頓時(shí)間。

Sun JDK提供的HotSpot JVM就能支持增量式GC。HotSpot JVM缺省GC方式為不使用增量GC,為了啟動增量GC,我們必須在運(yùn)行Java程序時(shí)增加-Xincgc的參數(shù)。

HotSpot JVM增量式GC的實(shí)現(xiàn)是采用Train GC算法,它的基本想法就是:將堆中的所有對象按照創(chuàng)建和使用情況進(jìn)行分組(分層),將使用頻繁高和具有相關(guān)性的對象放在一隊(duì)中,隨著程序的運(yùn)行,不斷對組進(jìn)行調(diào)整。當(dāng)GC運(yùn)行時(shí),它總是先回收最老的(最近很少訪問的)的對象,如果整組都為可回收對象,GC將整組回收。這樣,每次GC運(yùn)行只回收一定比例的不可達(dá)對象,保證程序的順暢運(yùn)行。


六、詳解函數(shù)finalize

finalize 是位于Object類的一個(gè)方法,詳見我的開源項(xiàng)目:src-jdk1.7.0_02

protected?void?finalize()?throws?Throwable?{?}

該方法的訪問修飾符為protected,由于所有類為Object的子類,因此用戶類很容易訪問到這個(gè)方法。

由于,finalize函數(shù)沒有自動實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,我們必須手動的實(shí)現(xiàn),因此finalize函數(shù)的最后一個(gè)語句通常是 super.finalize()。通過這種方式,我們可以實(shí)現(xiàn)從下到上實(shí)現(xiàn)finalize的調(diào)用,即先釋放自己的資源,然后再釋放父類的資源。根據(jù)Java語言規(guī)范,JVM保證調(diào)用finalize函數(shù)之前,這個(gè)對象是不可達(dá)的,但是JVM不保證這個(gè)函數(shù)一定會被調(diào)用。另外,規(guī)范還保證finalize函數(shù)最多運(yùn)行一次。

很多Java初學(xué)者會認(rèn)為這個(gè)方法類似與C++中的析構(gòu)函數(shù),將很多對象、資源的釋放都放在這一函數(shù)里面。其實(shí),這不是一種很好的方式,原因有三:

其一、GC為了能夠支持finalize函數(shù),要對覆蓋這個(gè)函數(shù)的對象作很多附加的工作。

其二、在finalize運(yùn)行完成之后,該對象可能變成可達(dá)的,GC還要再檢查一次該對象是否是可達(dá)的。因此,使用 finalize會降低GC的運(yùn)行性能。

其三、由于GC調(diào)用finalize的時(shí)間是不確定的,因此通過這種方式釋放資源也是不確定的。


通常,finalize用于一些不容易控制、并且非常重要資源的釋放,例如一些I/O的操作,數(shù)據(jù)的連接。這些資源的釋放對整個(gè)應(yīng)用程序是非常關(guān)鍵的。在這種情況下,程序員應(yīng)該以通過程序本身管理(包括釋放)這些資源為主,以finalize函數(shù)釋放資源方式為輔,形成一種雙保險(xiǎn)的管理機(jī)制,而不應(yīng)該僅僅依靠finalize來釋放資源。

下面給出一個(gè)例子說明,finalize函數(shù)被調(diào)用以后,仍然可能是可達(dá)的,同時(shí)也可說明一個(gè)對象的finalize只可能運(yùn)行一次。


class MyObject {

Test main; // 記錄Test對象,在finalize中時(shí)用于恢復(fù)可達(dá)性

public MyObject(Test t) {

main = t; // 保存Test 對象

}

protected void finalize() {

main.ref = this; // 恢復(fù)本對象,讓本對象可達(dá)

System.out.println("This is finalize"); // 用于測試finalize只運(yùn)行一次

}

}

class Test {

MyObject ref;

public static void main(String[] args) {

Test test = new Test();

test.ref = new MyObject(test);

test.ref = null; // MyObject對象為不可達(dá)對象,finalize將被調(diào)用

System.gc();

if (test.ref != null)

System.out.println("My Object還活著");

}

}


運(yùn)行結(jié)果:

  This is finalize

  My Object還活著

此例子中需要注意,雖然MyObject對象在finalize中變成可達(dá)對象,但是下次回收時(shí)候,finalize卻不再被調(diào)用,因?yàn)閒inalize函數(shù)最多只調(diào)用一次。


七、GC程序交互

程序如何與GC進(jìn)行交互呢??Java2增強(qiáng)了內(nèi)存管理功能,增加了一個(gè)java.lang.ref包,詳見我的開源項(xiàng)目:src-jdk1.7.0_02

其中定義了三種引用類。這三種引用類分別為:SoftReference、 WeakReference、 PhantomReference

通過使用這些引用類,程序員可以在一定程度與GC進(jìn)行交互,以便改善GC的工作效率,這些引用類的引用強(qiáng)度介于可達(dá)對象和不可達(dá)對象之間。

創(chuàng)建一個(gè)引用對象也非常容易,例如:如果你需要創(chuàng)建一個(gè)Soft Reference對象,那么首先創(chuàng)建一個(gè)對象,并采用普通引用方式(可達(dá)對象);然后再創(chuàng)建一個(gè)SoftReference引用該對象;最后將普通引用設(shè)置為null。通過這種方式,這個(gè)對象就只有一個(gè)Soft Reference引用。同時(shí),我們稱這個(gè)對象為Soft Reference 對象。

Soft Reference的主要特點(diǎn)是據(jù)有較強(qiáng)的引用功能。只有當(dāng)內(nèi)存不夠的時(shí)候,才進(jìn)行回收這類內(nèi)存,因此在內(nèi)存足夠的時(shí)候,它們通常不被回收。另外,這些引用對象還能保證在Java拋出OutOfMemory 異常之前,被設(shè)置為null。它可以用于實(shí)現(xiàn)一些常用圖片的緩存,實(shí)現(xiàn)Cache的功能,保證最大限度的使用內(nèi)存而不引起OutOfMemory。以下給出這種引用類型的使用偽代碼:


// 申請一個(gè)圖像對象

  Image image=new Image(); // 創(chuàng)建Image對象

  …

  // 使用 image

  …

  // 使用完了image,將它設(shè)置為soft 引用類型,并且釋放強(qiáng)引用;

  SoftReference sr=new SoftReference(image);

  image=null;

  …

  // 下次使用時(shí)

  if (sr!=null)

image=sr.get();

  else{

   image=new Image(); //由于GC由于低內(nèi)存,已釋放image,因此需要重新裝載;

   sr=new SoftReference(image);

  }


Weak引用對象與Soft引用對象的最大不同就在于:GC在進(jìn)行回收時(shí),需要通過算法檢查是否回收Soft引用對象,而對于Weak引用對象,GC總是進(jìn)行回收。Weak引用對象更容易、更快被GC回收。雖然,GC在運(yùn)行時(shí)一定回收Weak對象,但是復(fù)雜關(guān)系的Weak對象群常常需要好幾次GC的運(yùn)行才能完成。Weak引用對象常常用于Map結(jié)構(gòu)中,引用數(shù)據(jù)量較大的對象,一旦該對象的強(qiáng)引用為null時(shí),GC能夠快速地回收該對象空間。

Phantom引用的用途較少,主要用于輔助finalize函數(shù)的使用。Phantom對象指一些對象,它們執(zhí)行完了finalize函數(shù),并為不可達(dá)對象,但是它們還沒有被GC回收。這種對象可以輔助finalize進(jìn)行一些后期的回收工作,我們通過覆蓋Reference的clear()方法,增強(qiáng)資源回收機(jī)制的靈活性。


八、Java編程建議

根據(jù)GC的工作原理,我們可以通過一些技巧和方式,讓GC運(yùn)行更加有效率,更加符合應(yīng)用程序的要求。一些關(guān)于程序設(shè)計(jì)的幾點(diǎn)建議:

1)最基本的建議就是盡早釋放無用對象的引用。大多數(shù)程序員在使用臨時(shí)變量的時(shí)候,都是讓引用變量在退出活動域(scope)后,自動設(shè)置為 null.我們在使用這種方式時(shí)候,必須特別注意一些復(fù)雜的對象圖,例如數(shù)組,隊(duì)列,樹,圖等,這些對象之間有相互引用關(guān)系較為復(fù)雜。對于這類對象,GC 回收它們一般效率較低。如果程序允許,盡早將不用的引用對象賦為null,這樣可以加速GC的工作。

2)盡量少用finalize函數(shù)。finalize函數(shù)是Java提供給程序員一個(gè)釋放對象或資源的機(jī)會。但是,它會加大GC的工作量,因此盡量少采用finalize方式回收資源。

3)如果需要使用經(jīng)常使用的圖片,可以使用soft應(yīng)用類型。它可以盡可能將圖片保存在內(nèi)存中,供程序調(diào)用,而不引起OutOfMemory.

4)注意集合數(shù)據(jù)類型,包括數(shù)組,樹,圖,鏈表等數(shù)據(jù)結(jié)構(gòu),這些數(shù)據(jù)結(jié)構(gòu)對GC來說,回收更為復(fù)雜。另外,注意一些全局的變量,以及一些靜態(tài)變量。這些變量往往容易引起懸掛對象(dangling reference),造成內(nèi)存浪費(fèi)。

5)當(dāng)程序有一定的等待時(shí)間,程序員可以手動執(zhí)行System.gc(),通知GC運(yùn)行,但是Java語言規(guī)范并不保證GC一定會執(zhí)行。使用增量式GC可以縮短Java程序的暫停時(shí)間。

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

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

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡欲明心閱讀 90,359評論 17 311
  • 原文閱讀 前言 這段時(shí)間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點(diǎn)贊,這博客我...
    碼農(nóng)戲碼閱讀 6,150評論 2 31
  • 最近學(xué)習(xí)Python的GC機(jī)制時(shí),想到了java的GC,忘得差不多了,(⊙﹏⊙)b!!這里便做一下回顧總結(jié)。推薦周...
    廿陸小生閱讀 1,267評論 0 0
  • 記得大學(xué)的時(shí)候,有遇到過很有靈性的老師。為什么用靈性來形容老師,這是個(gè)值得討論的問題。一般我們形容老師,都會想到一...
    守望者語閱讀 314評論 1 0
  • 我開始害怕未來了。我已經(jīng)可以預(yù)見自己的未來了,然而并不是什么好事。因?yàn)?,我可以看見自己之后的工作,一直沉淪在低等職...
    白色小豬閱讀 184評論 0 0

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