Java中的垃圾回收通常是在Java堆中進行,由於堆中幾乎存放了Java中全部的對象實例。談到Java堆中的垃圾回收,天然要談到引用。在JDK1.2以前,Java中的引用定義很很純粹:若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。但在JDK1.2以後,Java對引用的概念進行了擴充,將其分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。java
Java堆中存放着幾乎全部的對象實例,垃圾收集器對堆中的對象進行回收前,要先肯定這些對象是否還有用,斷定對象是否爲垃圾對象有以下算法:算法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1,任什麼時候刻計數器都爲0的對象就是不可能再被使用的。編程
引用計數算法的實現簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的選擇,當Java語言並無選擇這種算法來進行垃圾回收,主要緣由是它很難解決對象之間的相互循環引用問題。數組
Java和C#中都是採用根搜索算法來斷定對象是否存活的。這種算法的基本思路是經過一系列名爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,就證實此對象是不可用的。在Java語言裏,可做爲GC Roots的兌現包括下面幾種:服務器
實際上,在根搜索算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行根搜索後發現沒有與GC Roots相鏈接的引用鏈,那它會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行。若是該對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue隊列中,並在稍後由一條由虛擬機自動創建的、低優先級的Finalizer線程去執行finalize()方法。finalize()方法是對象逃脫死亡命運的最後一次機會(由於一個對象的finalize()方法最多隻會被系統自動調用一次),稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是要在finalize()方法中成功拯救本身,只要在finalize()方法中讓該對象重引用鏈上的任何一個對象創建關聯便可。而若是對象這時尚未關聯到任何鏈上的引用,那它就會被回收掉。網絡
斷定除了垃圾對象以後,即可以進行垃圾回收了。下面介紹一些垃圾收集算法,因爲垃圾收集算法的實現涉及大量的程序細節,所以這裏主要是闡明各算法的實現思想,而不去細論算法的具體實現。ide
標記—清除算法是最基礎的收集算法,它分爲「標記」和「清除」兩個階段:首先標記出所需回收的對象,在標記完成後統一回收掉全部被標記的對象,它的標記過程其實就是前面的根搜索算法中斷定垃圾對象的標記過程。標記—清除算法的執行狀況以下圖所示:性能
回收前狀態:優化
該算法有以下缺點:spa
複製算法是針對標記—清除算法的缺點,在其基礎上進行改進而獲得的,它講課用內存按容量分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活着的對象複製到另一塊內存上面,而後再把已使用過的內存空間一次清理掉。複製算法有以下優勢:
它的缺點是:可一次性分配的最大內存縮小了一半。
複製算法的執行狀況以下圖所示:
回收前狀態:
複製算法比較適合於新生代,在老年代中,對象存活率比較高,若是執行較多的複製操做,效率將會變低,因此老年代通常會選用其餘算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程同樣,但對標記後出的垃圾對象的處理狀況有所不一樣,它不是直接對可回收對象進行清理,而是讓全部的對象都向一端移動,而後直接清理掉端邊界之外的內存。標記—整理算法的回收狀況以下所示:
回收前狀態:
回收後狀態:
當前商業虛擬機的垃圾收集 都採用分代收集,它根據對象的存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少許存活,所以可選用複製算法來完成收集,而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。
下面咱們來看以下代碼:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 byte[] holder = new byte[32*1024*1024]; 4 System.gc(); 5 } 6 }
[GC 208K->134K(5056K), 0.0017306 secs]
[Full GC 134K->134K(5056K), 0.0121194 secs]
[Full GC 32902K->32902K(37828K), 0.0094149 sec
注意第三行,「->」以前的數據表示垃圾回收前堆中存活對象所佔用的內存大小,「->」以後的數據表示垃圾回收堆中存活對象所佔用的內存大小,括號中的數據表示堆內存的總容量,0.0094149 sec 表示垃圾回收所用的時間。
從結果中能夠看出,System.gc(()運行後並無回收掉這32MB的內存,這應該是意料之中的結果,由於變量holder還處在做用域內,虛擬機天然不會回收掉holder引用的對象所佔用的內存。
咱們把代碼修改以下:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 { 4 byte[] holder = new byte[32*1024*1024]; 5 } 6 System.gc(); 7 } 8 }
加入花括號後,holder的做用域被限制在了花括號以內,所以,在執行System.gc()時,holder引用已經不能再被訪問,邏輯上來說,此次應該會回收掉holder引用的對象所佔的內存。但查看垃圾回收狀況時,輸出信息以下:
[GC 208K->134K(5056K), 0.0017100 secs]
[Full GC 134K->134K(5056K), 0.0125887 secs]
[Full GC 32902K->32902K(37828K), 0.0089226 secs]
很明顯,這32MB的數據並無被回收。下面咱們再作以下修改:
1 public class SlotGc{ 2 public static void main(String[] args){ 3 { 4 byte[] holder = new byte[32*1024*1024]; 5 holder = null; 6 } 7 System.gc(); 8 } 9 }
此次獲得的垃圾回收信息以下:
[GC 208K->134K(5056K), 0.0017194 secs]
[Full GC 134K->134K(5056K), 0.0124656 secs]
[Full GC 32902K->134K(37828K), 0.0091637 secs]
說明此次holder引用的對象所佔的內存被回收了。咱們慢慢來分析。
首先明確一點:holder可否被回收的根本緣由是局部變量表中的Slot是否還存有關於holder數組對象的引用。
在第一次修改中,雖然在holder做用域以外進行回收,可是在此以後,沒有對局部變量表的讀寫操做,holder所佔用的Slot尚未被其餘變量所複用(回憶Java內存區域與內存溢出一文中關於Slot的講解),因此做爲GC Roots一部分的局部變量表仍保持者對它的關聯。這種關聯沒有被及時打斷,所以GC收集器不會將holder引用的對象內存回收掉。 在第二次修改中,在GC收集器工做前,手動將holder設置爲null值,就把holder所佔用的局部變量表中的Slot清空了,所以,此次GC收集器工做時將holder以前引用的對象內存回收掉了。
固然,咱們也能夠用其餘方法來將holder引用的對象內存回收掉,只要複用holder所佔用的slot便可,好比在holder做用域以外執行一次讀寫操做。
爲對象賦null值並非控制變量回收的最好方法,以恰當的變量做用域來控制變量回收時間纔是最優雅的解決辦法。另外,賦null值的操做在通過虛擬機JIT編譯器優化後會被消除掉,通過JIT編譯後,System.gc()執行時就能夠正確地回收掉內存,而無需賦null值。
Java虛擬機的內存管理與垃圾收集是虛擬機結構體系中最重要的組成部分,對程序(尤爲服務器端)的性能和穩定性有着很是重要的影響。性能調優須要具體狀況具體分析,並且實際分析時可能須要考慮的方面不少,這裏僅就一些簡單經常使用的狀況做簡要介紹。
除了Java堆和永久代以及直接內存外,還要注意下面這些區域也會佔用較多的內存,這些內存的總和會受到操做系統進程最大內存的限制:
一、線程堆棧:可經過-Xss調整大小,內存不足時拋出StackOverflowError(縱向沒法分配,即沒法分配新的棧幀)或OutOfMemoryError(橫向沒法分配,即沒法創建新的線程)。
二、Socket緩衝區:每一個Socket鏈接都有Receive和Send兩個緩衝區,分別佔用大約37KB和25KB的內存。若是沒法分配,可能會拋出IOException:Too many open files異常。關於Socket緩衝區的詳細介紹參見個人Java網絡編程系列中深刻剖析Socket的幾篇文章。
三、JNI代碼:若是代碼中使用了JNI調用本地庫,那本地庫使用的內存也不在堆中。
四、虛擬機和GC:虛擬機和GC的代碼執行也要消耗必定的內存。
轉自:http://blog.csdn.net/ns_code/article/details/18076173