天天看點

java記憶體管理和回收機制

java類檔案是以 .java為字尾的檔案,經過javac指令編譯後,編譯成class檔案,class檔案中都是二進制格式的資料,是以想要看編譯後的内容是什麼,可以采用jdk自帶的javap指令檢視。

JVM中有個組成部分為類加載器(ClassLoader),負責java檔案編譯後class檔案的加載,加載到哪呢,加載到記憶體。那下面來說一下JVM的記憶體管理。

java通過類加載器來加載class檔案,加載到記憶體後,會把類、方法、常變量放到堆記憶體中。因為java是自動進行垃圾回收的,是以放入堆記憶體中的東西,哪些該回收,哪些不該回收?這都需要jvm去額外的線程去進行判斷。但如果對于一個大型的J2EE系統來說,當建立的對象及方法變量比較多時,即堆記憶體中的對象比較多,如果一個一個對象去進行循環判斷是否該回收時,這樣的回收機制未免太耗時了,系統的性能一定會下降,jvm為了提高jvm執行效率,采用了堆記憶體分區管理的機制。

JVM的記憶體分棧記憶體、堆記憶體、本地方法棧和方法區四部分。

java記憶體管理和回收機制

1)堆

所有通過new建立的對象的記憶體都在堆中配置設定,其大小可以通過-Xmx和-Xms來控制。堆被劃分為新生代和舊生代,新生代又被進一步劃分為Eden和Survivor區,最後Survivor由FromSpace和ToSpace組成,結構圖如下所示:

JVM把堆記憶體分三大塊:Young Generation Space 新生區(也稱新生代)、Tenure generation space養老區(也稱舊生代)、Permanent Space 永久存儲區。分區是為了進行子產品化管理,管理不同的對象及變量以提高JVM的執行效率。

對于Young Generation Space ,它主要用來存儲新建立的對象,記憶體大小會比較小,垃圾回收會比較頻繁。對此區又分三個區域:一個Eden Space和兩個Survivor Space。有一個前輩對這三個區域描述的相當透徹:

當對象在堆建立時,将進入年輕代的Eden Space。

垃圾回收器進行垃圾回收時,掃描Eden Space和A Suvivor Space,如果對象仍然存活,則複制到B Suvivor Space,如果B Suvivor Space已經滿,則複制 Old Gen

掃描A Suvivor Space時,如果對象已經經過了幾次的掃描仍然存活,JVM認為其為一個Old對象,則将其移到Old Gen。

掃描完畢後,JVM将Eden Space和A Suvivor Space清空,然後交換A和B的角色(即下次垃圾回收時會掃描Eden Space和BSuvivor Space。

 對于Tenure generation space,它主要是用來存儲那些長時間被引用的對象。因為它裡面存放的是經過幾次在Young Genderation Space 進行掃描判斷過仍存活的對象,記憶體大小會比較大,垃圾回收頻率會比較小。

 對于Permanent Space 永久存儲區,它是用來存儲一些值資訊不經常變更的東東,有類定義、位元組碼和常量等。

java記憶體管理和回收機制

新生代。建立的對象都是用新生代配置設定記憶體,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例舊生代。用于存放新生代中經過多次垃圾回收仍然存活的對象

2)棧

每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變量區和操作數棧,用于存放此次方法調用過程中的臨時變量、參數和中間結果

3)本地方法棧

用于支援native方法的執行,存儲了每個native方法調用的狀态

4)方法區

存放了要加載的類資訊、靜态變量、final類型的常量、屬性和方法資訊。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值。

記憶體回收

介紹完了JVM記憶體組成結構,下面我們再來看一下JVM垃圾回收機制。JVM分别對新生代和舊生代采用不同的垃圾回收機制

新生代的GC:

新生代通常存活時間較短,是以基于Copying算法來進行回收,所謂Copying算法就是掃描出存活的對象,并複制到一塊新的完全未使用的空間中,對應于新生代,就是在Eden和FromSpace或ToSpace之間copy。新生代采用空閑指針的方式來控制GC觸發,指針保持最後一個配置設定的對象在新生代區間的位置,當有新的對象要配置設定記憶體時,用于檢查空間是否足夠,不夠就觸發GC。當連續配置設定對象時,對象會逐漸從eden到

survivor,最後到舊生代,

用javavisualVM來檢視,能明顯觀察到新生代滿了後,會把對象轉移到舊生代,然後清空繼續裝載,當舊生代也滿了後,就會報outofmemory的異常,如下圖所示:

java記憶體管理和回收機制

在執行機制上JVM提供了串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

1)串行GC

在整個掃描和複制過程采用單線程的方式來進行,适用于單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client級别預設的GC方式,可以通過-XX:+UseSerialGC來強制指定

2)并行回收GC

在整個掃描和複制過程采用多線程的方式來進行,适用于多CPU、對暫停時間要求較短的應用上,是server級别預設采用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數

3)并行GC

與舊生代的并發GC配合使用

舊生代的GC:

舊生代與新生代不同,對象存活的時間比較長,比較穩定,是以采用标記(Mark)算法來進行回收,所謂标記就是掃描出存活的對象,然後再進行回收未被标記的對象,回收後對用空出的空間要麼進行合并,要麼标記出來便于下次進行配置設定,總之就是要減少記憶體碎片帶來的效率損耗。在執行機制上JVM提供了串行 GC(SerialMSC)、并行GC(parallelMSC)和并發GC(CMS),具體算法細節還有待進一步深入研究。

以上各種GC機制是需要組合使用的,指定方式由下表所示:

java記憶體管理和回收機制

gc()有何用?

  調用 gc 方法暗示着 Java 虛拟機做了一些努力來回收未用對象,以便能夠快速地重用這些對象目前占用的記憶體。當控制權從方法調用中傳回時,虛拟機已經盡最大努力從所有丢棄的對象中回收了空間。 

調用 System.gc() 等效于調用Runtime.getRuntime().gc()

finalize()有何用?

舉個例子來說,當java 調用非java方法時(這種方法可能是c或是c++的),在非java代碼内部也許調用了c的malloc()函數來配置設定記憶體,而且除非調用那個了 free() 否則不會釋放記憶體(因為free()是c的函數),這個時候要進行釋放記憶體的工作,gc是不起作用的,因而需要在finalize()内部的一個固有方法調用free()。

finalize的工作原理應該是這樣的:一旦垃圾收集器準備好釋放對象占用的存儲空間,它首先調用finalize(),而且隻有在下一次垃圾收集過程中,才會真正回收對象的記憶體.是以如果使用finalize(),就可以在垃圾收集期間進行一些重要的清除或清掃工作.

finalize()在什麼時候被調用?

有三種情況

1.所有對象被Garbage Collection時自動調用,比如運作System.gc()後.

2.程式退出時為每個對象調用一次finalize方法。

3.顯式的調用finalize方法

除此以外,正常情況下,當某個對象被系統收集為無用資訊的時候,finalize()将被自動調用。但是jvm不保證finalize()一定被調用,也就是說,finalize()的調用是不确定的,這也就是為什麼sun不提倡使用finalize()的原因. 簡單來講,finalize()是在對象被GC回收前會調用的方法,而System.gc()建議而非強制GC開始回收工作,具體執行要看GC的執行政策。

調用了 System.gc() 之後,java 在記憶體回收過程中就會調用那些要被回收的對象的 finalize() 方法。