JVM堆相關知識 為什么先說JVM堆?
JVM的堆是Java對象的活動空間,程序中的類的對象從中分配空間,其存儲着正在運行着的應用程序用到的所有對象。這些對象的建立方式就是那些new一類的操作,當對象無用后,是GC來負責這個無用的對象。
JVM堆
(1) 新域:存儲所有新成生的對象 新域會被分為3個部分:1.第一個部分叫Eden。2.另兩個部分稱為輔助生存空間(幼兒園),我這里一個稱為A空間(From sqace),一個稱為B空間(To Space)。
(2) 舊域:新域中的對象,經過了一定次數的GC循環后,被移入舊域
(3)永久域:存儲類和方法對象,從配置的角度看,這個域是獨立的,不包括在JVM堆內。默認為4M。�
垃圾回收的原因
從計算機組成的角度來講,所有的程序都是要駐留在內存中運行的。而內存是一個限制因素(大小)。除此之外,托管堆也有大小限制。因為地址空間和存儲的限制因素,托管堆要通過垃圾回收機制,來維持它的正常運作,保證對象的分配,盡可能不造成“內存溢出”。
垃圾回收的基本原理(算法思路都是一致的:把所有對象組成一個集合,或可以理解為樹狀結構,從樹根開始找,只要可以找到的都是活動對象,如果找不到,這個對象就被回收了)
垃圾回收分為兩個階段:
標記 --> 壓縮標記的過程,其實就是判斷對象是否可達的過程。當所有的根都檢查完畢后,堆中將包含可達(已標記)與不可達(未標記)對象。標記完成后,進入壓縮階段。在這個階段中,垃圾回收器線性的遍歷堆,以尋找不可達對象的連續內存塊。並把可達對象移動到這里以節約內存空間。
垃圾收集算法
Mark-Sweep標記清理算法
階段1: Mark-Sweep 標記清除階段,先假設heap中所有對象都可以回收,然后找出不能回收的對象,給這些對象打上標記,最后heap中沒有打標記的對象都是可以被回收的;
階段2: Compact 壓縮階段,對象回收之后heap內存空間變得不連續,在heap中移動這些對象,使他們重新從heap基地址開始連續排列(節省內存資源)。
Heap內存經過回收、壓縮之后,可以繼續采用前面的heap內存分配方法, 即僅用一個指針記錄heap分配的起始地址就可以。主要處理步驟:將線程掛起→確定roots→創建reachable objects graph→對象回收→heap壓縮→指針修復。可以這樣理解roots:heap中對象的引用關系錯綜復雜(交叉引用、循環引用),形成復雜的 graph,roots是CLR在heap之外可以找到的各種入口點。
GC搜索roots的地方包括全局對象、靜態變量、局部對象、函數調用參數、當前CPU寄存器中的對象指針(還有finalization queue)等。主要可以歸為2種類型:已經初始化了的靜態變量、線程仍在使用的對象(stack+CPU register)
指針修復是因為compact過程移動了heap對象,對象地址發生變化,需要修復所有引用指針,包括stack、CPU register中的指針以及heap中其他對象的引用指針。
復制算法
新生代的內存被划分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活着的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例為8:1,意思是每次新生代中可用內存空間為整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)。
標記整理算法
與標記清理算法過程一樣,只是不直接清理可回收對象,而是將所有存活對象移動到一端,之后清理邊界之外的對象內存
分代收集算法
現代商用虛擬機基本都采用分代收集算法來進行垃圾回收。這種算法沒什么特別的,無非是上面內容的結合罷了,根據對象的生命周期的不同將內存划分為幾塊,然后根據各塊的特點采用最適當的收集算法。大批對象死去、少量對象存活的(新生代),使用復制算法,復制成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),采用標記-清理算法或者標記-整理算法。
如何找到需要回收的對象
1、引用計數法:給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值為0的對象就是不可能再被使用的。
2、可達性分析法:對於可達性分析算法而言,未到達的對象並非是“非死不可”的,若要宣判一個對象死亡,至少需要經歷兩次標記階段。
1. 如果對象在進行可達性分析后發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件為是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法已經被虛擬機執行過了( finalize()在什么時候被調用? 有三種情況 1.所有對象被Garbage Collection時自動調用,比如運行System.gc()的時候. 2.程序退出時為每個對象調用一次finalize方法。 3.顯式的調用finalize方法),則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,那么,這個對象會被放置在一個叫F-Queue的隊列中,之后會由虛擬機自動建立的、優先級低的Finalizer線程去執行,而虛擬機不必要等待該線程執行結束,即虛擬機只負責建立線程,其他的事情交給此線程去處理。
2.對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變量,那么在第二次標記的時候該對象將從“即將回收”的集合中移除,如果對象還是沒有拯救自己,那就會被回收。它只能拯救自己一次,第二次就被回收了。此外,從我們可以得知,一個堆對象的this引用會永遠存在,在方法體內可以將this引用賦值給其他變量,這樣堆中對象就可以被其他變量所引用,即不會被回收.
Java有了GC同樣會出現內存泄露問題
1.靜態集合類像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變量的生命周期和應用程序一致,所有的對象Object也不能被釋放,因為他們也將一直被Vector等應用着。
2.各種連接,數據庫連接,網絡連接,IO連接等沒有顯示調用close關閉,不被GC回收導致內存泄露。
3.監聽器的使用,在釋放對象的同時沒有相應刪除監聽器的時候也可能導致內存泄露。
垃圾回收器負責回收所有無任何引用對象的內存空間。
注意:垃圾回收回收的是無任何引用的對象占據的內存空間而不是對象本身。
GC注意事項:
1、只管理內存,非托管資源,如文件句柄,GDI資源,數據庫連接等還需要用戶去管理。
2、循環引用,網狀結構等的實現會變得簡單。GC的標志-壓縮算法能有效的檢測這些關系,並將不再被引用的網狀結構整體刪除。
3、GC通過從程序的根對象開始遍歷來檢測一個對象是否可被其他對象訪問,而不是用類似於COM中的引用計數方法。
4、GC在一個獨立的線程中運行來刪除不再被引用的內存。
5、GC每次運行時會壓縮托管堆。
GC總結
[if !supportLists]1. [endif]JVM堆的大小決定了GC的運行時間。如果JVM堆的大小超過一定的限度,那么GC的運行時間會很長。2.對象生存的時間越長,GC需要的回收時間也越長,影響了回收速度。3.大多數對象都是短命的,所以,如果能讓這些對象的生存期在GC的一次運行周期內,wonderful!4.應用程序中,建立與釋放對象的速度決定了垃圾收集的頻率。5.如果GC一次運行周期超過3-5秒,這會很影響應用程序的運行,如果可以,應該減少JVM堆的大小了。6.前輩經驗之談:通常情況下,JVM堆的大小應為物理內存的80%。