首先JVM的記憶體結構包括五大區域: 程式計數器、虛擬機棧、本地方法棧、方法區、堆區。其中程式計數器、虛擬機棧和本地方法棧3個區域隨線程啟動與銷毀, 因此這幾個區域的記憶體分配和回收都具有確定性,不需要過多考慮回收的問題。而Java堆區和方法區則不一樣,這部分記憶體的分配和回收是動態的,正式垃圾回收需要關 ...
首先JVM的記憶體結構包括五大區域: 程式計數器、虛擬機棧、本地方法棧、方法區、堆區。其中程式計數器、虛擬機棧和本地方法棧3個區域隨線程啟動與銷毀, 因此這幾個區域的記憶體分配和回收都具有確定性,不需要過多考慮回收的問題。而Java堆區和方法區則不一樣,這部分記憶體的分配和回收是動態的,正式垃圾回收需要關註的部分。
垃圾回收在堆記憶體進行回收前, 要先確定區域的哪些對象是可以被回收的、那些對象暫時還不能回收,下麵談一談判斷對象是否存活的演算法。
判斷對象是否存活的演算法
1.引用計數演算法
引用計數演算法:堆中的每個對象實例都有一個引用計數器,當一個對象被創建時,就將該對象實例分配給一個變數,該引用計數器設置為1,當任何其他變數被賦值為這個對象的引用時,計數加1,當一個對象實例的某個引用超過了生命周期或被賦為一個新值時, 引用計數減1。
任何引用計數器為0的對象實例都可以進行垃圾回收。當一個對象實例被垃圾回收時,它引用的所有對象實例引用計數器減1.
優點:引用計數器可以很快的執行,對程式不需要長時間的打斷
缺點:無法檢測出迴圈引用。如對象A有對象B的引用,對象B又有對象A的引用,這樣他們的引用計數永遠都不為0
2.可達性分析演算法
可達性演算法:將所有的引用關係看作一張圖,從一個節點GC Root開始,尋找對應的引用節點,找到後繼續尋找這個節點的引用節點,當所有引用節點尋找完畢後,剩餘的節點就被認為是沒有被引用的節點,即無用節點,無用節點被判定為可回收對象。
Java中可以作為GC Root的包括下麵幾種:
- 虛擬機棧中的引用對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中引用的對象
對於Java中的引用類型可以看這篇文章Java 控制類的引用類型,合理使用記憶體
常用的垃圾回收演算法
1.標記-清除演算法
標記-清除演算法採用從根集合(GC Roots)進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行垃圾回收
這種演算法實現起來比較容易,但是會造成記憶體碎片
2.標記-複製演算法
複製演算法是為瞭解決標記-清除演算法的缺陷而提出的。
它將記憶體劃分為大小相等的兩塊,每次只使用其中的一塊。當這A快記憶體用完了,就將還存活的對象複製到B塊上面,然後把A塊的記憶體空間一次性清理掉
這種演算法雖然實現簡單,運行高效且不易產生記憶體碎片,但是卻對記憶體空間的使用做出了高昂的代價,因為能使用的空間縮減為原來的一半。很顯然,複製演算法的效率跟存活對象的數量有很大關聯,若存活對象很多,那麼效率將大大降低
3.標記-整理演算法
該演算法是為瞭解決複製演算法的缺陷,充分利用記憶體空間而提出的。
該演算法與標記-清除演算法一樣,但是在完成標記後,不直接清理可回收對象,而是將存活對象全部向一端移動,接著清理掉邊界以外的記憶體。
4.分代收集演算法
分代收集演算法是目前大部分JVM的垃圾收集器採用的演算法。其核心思想是根據對象存活的生命周期將記憶體劃分為若幹個不同的區域。
將其分為年輕代、老年代和永久代。然後根據不同的區域採用合適的收集演算法。
Java一般將堆區分為年輕代和老年代,將方法區劃為永久代。
下麵對不同的年齡代進行簡單說明
年輕代:新創建的對象都存放在這裡。因為年輕代會頻繁的進行GC清理,JVM在年輕代採用的是標記-複製演算法,先標記出存活的實例,然後清除掉無用實例,將存活的實例根據年齡(每個實例被經歷一次GC後年齡會加1)拷貝到不同的年齡代。
老年代:老年代中是經歷了N此垃圾禍首後仍然存活的對象,其中的N由JVM的參數決定。這塊記憶體區域一般大於年輕代。GC發生的次數也比年輕代要少。
永久代:用於存放靜態文件,如Java類、方法等。為方法區。
方法區主要回收的內容有:廢棄的常量、無用的類,對與廢棄常量可以同過引用的可達性判斷,但是對於無用類需要同時滿足以下3個條件:
- 該類的所有實例都已經被回收了
- 載入該類的 ClassLoader 已經被回收了
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
GC在什麼時候觸發
GC在優先順序最低的線程中運行,一般在應用程式空閑時被調用。當記憶體不足時才會主動調用
因為對象進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有如下兩種:
1.Scavenge GC
一般情況下,當新對象生成,並且在年輕代申請空間失敗時,會觸發Scavenge GC, 對年輕代進行垃圾回收。這種方式的GC不會影響到老年代。因為大部分對象都是年輕代開始的,同時年輕代記憶體不會分配的很大,所有年輕代的GC會頻繁的進行。所以在這裡要使用速度快、效率高的演算法,使其空間儘快空出來。
若GC一次後仍不能滿足記憶體分配,JVM會進行二次GC,若仍無法滿足,則報“out of memory"的錯誤,Java應用將停止
2.Full GC
對整個記憶體進行整理,包括年輕代、老年代和永久代,所以Full GC比Scavenge GC要慢, 因此應該儘量減少Full GC的次數。以下可能引發Full GC的原因:
- 老年代被寫滿
- 永久代被寫滿
- System.gc()被顯示調用
- 上一次GC後堆的各域分配策略動態變化。
Java的垃圾回收介紹到這,下麵在說說如何在程式中減少GC的開銷的幾個建議:
- 不要顯式調用System.gc()。此函數建議JVM進行GC,雖然只是建議,但是大多數情況下會觸發GC,增加了間歇性停頓的次數,大大影響系統的性能
- 儘量減少臨時對象的使用。也就是減少Scavenge GC執行的機會
- 對象不用時最好顯式置為null。將不用的對象置為null,有利於GC收集器判定,從而提高GC的效率
- 儘量減少靜態對象變數。靜態變數屬於全局變數,不會被GC禍首。
- 能有基本類型的就不要用包裝類。基本類型變數棧用的記憶體資源比對應的包裝類要少的多
- 使用StringBuffer 而不是String類累加字元串。因為堆String類型進行加的時候,會創建新的String對象,而StringBuffer是可變長的,在原有基礎上進行擴增,不會產生中間對象
- 分散對象創建或刪除的時間。集中在短時間內大量創建新對象,特別是大對象,會突然需要大量記憶體,JVM在面臨這種情況時只能進行GC,以回收記憶體或整合記憶體碎片,從而增加GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現了大量的垃圾對象,空閑空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會。