上一篇文章Java內存模型提到虛擬機所管理的內存主要包括如下幾個區域:程序計數器、虛擬機棧、本地方法棧、方法區和堆。其中前三個區域隨線程而生死,這些區域的內存分配和回收都具備肯定性。而堆和方法區則具備不肯定性,只有程序處於運行期間才能知道會建立哪些對象,本文主要討論這兩個部份內存的回收。html
Java的內存管理就是對象的分配和釋放問題。在Java中,經過關鍵字new爲每一個對象申請內存空間(基本類型除外),全部的對象都是在堆中分配空間,對象的釋放是由GC(Gabage Collection)決定和執行的。這種機制簡化了程序員工做的同時,也加劇的虛擬機的負荷,是Java程序運行速度較慢的緣由之一。java
咱們能夠將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象,另外每個線程對象能夠做爲一個圖的起始頂點,例如大多數程序從main進程開始執行,那麼該圖就是以main進程頂點開始的一棵根樹。在這個有向圖中,根節點可達的對象都是有效對象,GC將不回收這些對象,若是某個對象與這個根節點不可達,那麼咱們認爲這個對象再也不被引用,能夠被GC回收。eg:程序員
class Demo { public static void main(String[] args) { Object o1=new Object(); Object o2=new Object(); o2=o1; } }
在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特色,首先,這些對象是可達的,即在有向圖中,存在通路能夠與其相連;其次,這些對象是無用的,即程序之後不會再使用這些對象。若是對象知足這兩個條件,這些對象就能夠斷定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。下面是一個內存泄露的示例:算法
Vector v=new Vector(10); for (int i=1;i<100; i++) { Object o=new Object(); v.add(o); o=null; }
咱們循環申請Object對象,並將所申請的對象放入一個Vector中,若是咱們僅僅釋放引用自己,那麼Vector仍然引用該對象,因此這個對象對GC來講是不可回收的。所以,若是對象加入到Vector後,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置爲null。工具
經常使用的內存泄露工具包括工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。spa
引用計數法:給對象中添加一個引用計數器,每當一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。線程
主流的Java虛擬機並無選用上述方法來管理內存,主要緣由是它很難解決對象之間相互循環引用的問題。code
一個簡單的循環引用問題描述以下:有對象 A 和對象 B,對象 A 中含有對象 B 的引用,對象 B 中含有對象 A 的引用。此時,對象 A 和對象 B 的引用計數器都不爲 0。可是在系統中卻不存在任何第 3 個對象引用了 A 或 B。也就是說,A 和 B 是應該被回收的垃圾對象,但因爲垃圾對象間相互引用,從而使垃圾回收器沒法識別,引發內存泄漏。htm
class ReferenceCountingGc { public Object instance=null; public static void testGc() { ReferenceCountingGc objA=new ReferenceCountingGc(); ReferenceCountingGc objB=new ReferenceCountingGc(); objA.instance=objB; objB.instance=objA; objA=null; objB=null; } }
可達性分析法:主流的商用語言都用可達性分析算法來斷定對象是否存活。基本的思路是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲「引用鏈」,當一個對象到GC Roots沒有任何引用鏈相連時(即GC Roots到這個對象不可達),則證實此對象是不可用的。對象
在Java中,能夠做爲GC Roots的對象包括:
要真正宣告一個對象死亡,至少要通過兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記並而且進行第一次篩選,篩選的方法是此對象是否有必要執行finalize()方法(當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將視爲沒有必要執行),若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象就會被放置在一個叫作F-dequeu的隊列中,並稍後由一個低優先級的線程去執行它。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-dequeu中的對象進行第二次標記。
標記-清除算法:算法分爲標記和清除兩個階段,首先標記全部須要回收的對象,在標記完成之後統一回收全部被標記的對象。
這種算法有兩個問題,一個效率太低,另外一個是空間問題,標記清除後產生大量不連續的內存碎片,致使之後程序運行過程當中沒法找到足夠的連續內存。
複製算法:這中算法將可用內存按容量劃分爲大小相等的兩塊,每一次只使用其中的一塊,當這一塊的內存使用完了,就將還存活着的對象複製到另一塊上去,而後再將已使用過的內存空間一次清理掉。
Java 的新生代串行垃圾回收器中使用了複製算法的思想。新生代分爲 Eden 空間、From 空間、To 空間 3 個部分。其中 From 空間和 To 空間能夠視爲用於複製的兩塊大小相同、地位相等,且可進行角色互換的空間塊。From 和 To 空間也稱爲 survivor 空間,即倖存者空間,用於存放未被回收的對象。
在垃圾回收時,Eden 空間中的存活對象會被複制到未使用的 survivor 空間中 (假設是 to),正在使用的 survivor 空間 (假設是 from) 中的年輕對象也會被複制到 to 空間中 (大對象,或者老年對象會直接進入老年帶,若是 to 空間已滿,則對象也會直接進入老年代)。此時,Eden 空間和 From 空間中的剩餘對象就是垃圾對象,能夠直接清空,To 空間則存放這次回收後的存活對象。這種改進的複製算法既保證了空間的連續性,又避免了大量的內存空間浪費。
標記-整理算法:標記過程與標記-清除算法同樣,後續則是將全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
分代收集算法:根據對象存活週期的不一樣將內存劃分爲幾塊,通常是將對劃爲新生代和老年代,這樣能夠根據各個年代的特色採用使用的收集算法,在新生代中每次垃圾都發現大批對象死去,就採用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。
參考文獻:
一、深刻理解Java虛擬機,周志明,機械工業出版社
二、https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/