Java語言的一個重要特性是引入了自動的內存管理機制,使得開發人員不用本身來管理應用中的內存。C/C++開發人員須要經過malloc/free 和new/delete等函數來顯式的分配和釋放內存。這對開發人員提出了比較高的要求,容易形成內存訪問錯誤和內存泄露等問題。一個常見的問題是會產生「懸掛引用(dangling references)」,即一個對象引用所指向的內存區塊已經被錯誤的回收並從新分配給新的對象了,程序若是繼續使用這個引用的話會形成不可預期的結果。開發人員有可能忘記顯式的調用釋放內存的函數而形成內存泄露。而自動的內存管理則是把管理內存的任務交給編程語言的運行環境來完成。開發人員並不須要關心內存的分配和回收的底層細節。Java平臺經過垃圾回收器來進行自動的內存管理。html
Java的垃圾回收器要負責完成3件任務:分配內存、確保被引用的對象的內存不被錯誤回收以及回收再也不被引用的對象的內存空間。垃圾回收是一個複雜並且耗時的操做。若是JVM花費過多的時間在垃圾回收上,則勢必會影響應用的運行性能。通常狀況下,當垃圾回收器在進行回收操做的時候,整個應用的執行是被暫時停止(stop-the-world)的。這是由於垃圾回收器須要更新應用中全部對象引用的實際內存地址。不一樣的硬件平臺所能支持的垃圾回收方式也不一樣。好比在多CPU的平臺上,就能夠經過並行的方式來回收垃圾。而單CPU平臺則只能串行進行。不一樣的應用所指望的垃圾回收方式也會有所不一樣。服務器端應用可能但願在應用的整個運行時間中,花在垃圾回收上的時間總數越小越好。而對於與用戶交互的應用來講,則可能但願所垃圾回收所帶來的應用停頓的時間間隔越小越好。對於這種狀況,JVM中提供了多種垃圾回收方法以及對應的性能調優參數,應用能夠根據須要來進行定製。java
Java 垃圾回收機制最基本的作法是分代回收。內存中的區域被劃分紅不一樣的世代,對象根據其存活的時間被保存在對應世代的區域中。通常的實現是劃分紅3個世代:年輕、年老和永久。內存的分配是發生在年輕世代中的。當一個對象存活時間足夠長的時候,它就會被複制到年老世代中。對於不一樣的世代可使用不一樣的垃圾回收算法。進行世代劃分的出發點是對應用中對象存活時間進行研究以後得出的統計規律。通常來講,一個應用中的大部分對象的存活時間都很短。好比局部變量的存活時間就只在方法的執行過程當中。基於這一點,對於年輕世代的垃圾回收算法就能夠頗有針對性。linux
年輕世代的內存區域被進一步劃分紅伊甸園(Eden)和兩個存活區(survivor space)。Eden是進行內存分配的地方,是一塊連續的空閒內存區域。在上面進行內存分配速度很是快,由於不須要進行可用內存塊的查找。兩個存活區中始終有一個是空白的。在進行垃圾回收的時候,伊甸園和其中一個非空存活區中還存活的對象根據其存活時間被複制到當前空白的存活區或年老世代中。通過這一次的複製以後,以前非空的存活區中包含了當前還存活的對象,而伊甸園和另外一個存活區中的內容已經再也不須要了,只須要簡單地把這兩個區域清空便可。下一次垃圾回收的時候,這兩個存活區的角色就發生了交換。通常來講,年輕世代區域較小,並且大部分對象都已經再也不存活,所以在其中查找存活對象的效率較高。算法
而對於年老和永久世代的內存區域,則採用的是不一樣的回收算法,稱爲「標記-清除-壓縮(Mark-Sweep-Compact)」。標記的過程是找出當前還存活的對象,並進行標記;清除則遍歷整個內存區域,找出其中須要進行回收的區域;而壓縮則把存活對象的內存移動到整個內存區域的一端,使得另外一端是一塊連續的空閒區域,方便進行內存分配和複製。編程
JDK 5中提供了4種不一樣的垃圾回收機制。最經常使用的是串行回收方式,即便用單個CPU回收年輕和年老世代的內存。在回收的過程當中,應用程序被暫時停止。回收方式使用的是上面提到的最基本的分代回收。串行回收方式適合於通常的單CPU桌面平臺。若是是多CPU的平臺,則適合的是並行回收方式。這種方式在對年輕世代進行回收的時候,會使用多個CPU來並行處理,能夠提高回收的性能。併發標記-清除回收方式適合於對應用的響應時間要求比較 高的狀況,即須要減小垃圾回收所帶來的應用暫時停止的時間。這種作法的優勢在於能夠在應用運行的同時標記存活對象與回收垃圾,而只須要暫時停止應用比較短的時間。api
經過JDK中提供的JConsole能夠很容易的查看當前應用的內存使用狀況。在JVM啓動的時候添加參數 -verbose:gc 能夠查看垃圾回收器的運行結果。數組
若是一個內存中的對象沒有任何引用的話,就說明這個對象已經再也不被使用了,從而能夠成爲被垃圾回收的候選。不過因爲垃圾回收器的運行時間不肯定,可被垃圾回收的對象的實際被回收時間是不肯定的。對於一個對象來講,只要有引用的存在,它就會一直存在於內存中。若是這樣的對象愈來愈多,超出了JVM中的內存總數,JVM就會拋出OutOfMemory錯誤。雖然垃圾回收的具體運行是由JVM來控制的,可是開發人員仍然能夠在必定程度上與垃圾回收器進行交互,其目的在於更好的幫助垃圾回收器管理好應用的內存。這種交互方式就是使用JDK 1.2引入的java.lang.ref包。緩存
在通常的Java程序中,見到最多的就是強引用(strong reference)。如Date date = new Date(),date就是一個對象的強引用。對象的強引用能夠在程序中處處傳遞。不少狀況下,會同時有多個引用指向同一個對象。強引用的存在限制了對象在內存中的存活時間。假如對象A中包含了一個對象B的強引用,那麼通常狀況下,對象B的存活時間就不會短於對象A。若是對象A沒有顯式的把對象B的引用設爲null的話,就只有當對象A被垃圾回收以後,對象B纔再也不有引用指向它,纔可能得到被垃圾回收的機會。服務器
除了強引用以外,java.lang.ref包中提供了對一個對象的不一樣的引用方式。JVM的垃圾回收器對於不一樣類型的引用有不一樣的處理方式。併發
軟引用(soft reference)在強度上弱於強引用,經過類SoftReference來表示。它的做用是告訴垃圾回收器,程序中的哪些對象是不那麼重要,當內存不足的時候是能夠被暫時回收的。當JVM中的內存不足的時候,垃圾回收器會釋放那些只被軟引用所指向的對象。若是所有釋放完這些對象以後,內存還不足,纔會拋出OutOfMemory錯誤。軟引用可用來實現內存敏感的高速緩存。 軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收器回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。 軟引用很是適合於建立緩存。當系統內存不足的時候,緩存中的內容是能夠被釋放的。好比考慮一個圖像編輯器的程序。該程序會把圖像文件的所有內容都讀取到內存中,以方便進行處理。而用戶也能夠同時打開多個文件。當同時打開的文件過多的時候,就可能形成內存不足。若是使用軟引用來指向圖像文件內容的話,垃圾回收器就能夠在必要的時候回收掉這些內存。
public class ImageData { private String path; private SoftReference<byte[]> dataRef; public ImageData(String path) { this.path = path; dataRef = new SoftReference<byte[]>(new byte[0]); } private byte[] readImage() { return new byte[1024 * 1024]; //省略了讀取文件的操做 } public byte[] getData() { byte[] dataArray = dataRef.get(); if (dataArray == null || dataArray.length == 0) { dataArray = readImage(); dataRef = new SoftReference<byte[]>(dataArray); } return dataArray; } }
在運行上面程序的時候,可使用 -Xmx 參數來限制JVM可用的內存。因爲軟引用所指向的對象可能被回收掉,在經過get方法來獲取軟引用所實際指向的對象的時候,老是要檢查該對象是否還存活。
弱引用(weak reference)在強度上弱於軟引用,經過類WeakReference來表示。它的做用是引用一個對象,可是並不阻止該對象被回收。若是使用一個強引用的話,只要該引用存在,那麼被引用的對象是不能被回收的。弱引用則沒有這個問題。在垃圾回收器運行的時候,若是一個對象的全部引用都是弱引用的話,該對象會被回收。弱引用的做用在於解決強引用所帶來的對象之間在存活時間上的耦合關係。弱引用最多見的用處是在集合類中,尤爲在哈希表中。哈希表的接口容許使用任何Java對象做爲鍵來使用。當一個鍵值對被放入到哈希表中以後,哈希表對象自己就有了對這些鍵和值對象的引用。若是這種引用是強引用的話,那麼只要哈希表對象自己還存活,其中所包含的鍵和值對象是不會被回收的。若是某個存活時間很長的哈希表中包含的鍵值對不少,最終就有可能消耗掉JVM中所有的內存。
對於這種狀況的解決辦法就是使用弱引用來引用這些對象,這樣哈希表中的鍵和值對象都能被垃圾回收。Java中提供了WeakHashMap來知足這一常見需求。
在介紹幽靈引用以前,要先介紹Java提供的對象終止化機制(finalization)。在Object類裏面有個finalize方法,其設計的初衷是在一個對象被真正回收以前,能夠用來執行一些清理的工做。由於Java並無提供相似C++的析構函數同樣的機制,就經過 finalize方法來實現。可是問題在於垃圾回收器的運行時間是不固定的,因此這些清理工做的實際運行時間也是不能預知的。幽靈引用(phantom reference)能夠解決這個問題。在建立幽靈引用PhantomReference的時候必需要指定一個引用隊列。當一個對象的finalize方法已經被調用了以後,這個對象的幽靈引用會被加入到隊列中。經過檢查該隊列裏面的內容就知道一個對象是否是已經準備要被回收了。
幽靈引用及其隊列的使用狀況並很少見,主要用來實現比較精細的內存使用控制,這對於移動設備來講是頗有意義的。程序能夠在肯定一個對象要被回收以後,再申請內存建立新的對象。經過這種方式可使得程序所消耗的內存維持在一個相對較低的數量。好比下面的代碼給出了一個緩衝區的實現示例。
public class PhantomBuffer { private byte[] data = new byte[0]; private ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>(); private PhantomReference<byte[]> ref = new PhantomReference<byte[]>(data, queue); public byte[] get(int size) { if (size <= 0) { throw new IllegalArgumentException("Wrong buffer size"); } if (data.length < size) { data = null; System.gc(); //強制運行垃圾回收器 try { queue.remove(); //該方法會阻塞直到隊列非空 ref.clear(); //幽靈引用不會自動清空,要手動運行 ref = null; data = new byte[size]; ref = new PhantomReference<byte[]>(data, queue); } catch (InterruptedException e) { e.printStackTrace(); } } return data; } }
在上面的代碼中,每次申請新的緩衝區的時候,都首先確保以前的緩衝區的字節數組已經被成功回收。引用隊列的remove方法會阻塞直到新的幽靈引用被加入到隊列中。不過須要注意的是,這種作法會致使垃圾回收器被運行的次數過多,可能會形成程序的吞吐量太低。
在有些狀況下,程序會須要在一個對象的可達到性發生變化的時候獲得通知。好比某個對象的強引用都已經不存在了,只剩下軟引用或是弱引用。可是還須要對引用自己作一些的處理。典型的情景是在哈希表中。引用對象是做爲WeakHashMap中的鍵對象的,當其引用的實際對象被垃圾回收以後,就須要把該鍵值對從哈希表中刪除。有了引用隊列(ReferenceQueue),就能夠方便的獲取到這些弱引用對象,將它們從表中刪除。在軟引用和弱引用對象被添加到隊列以前,其對實際對象的引用會被自動清空。經過引用隊列的poll/remove方法就能夠分別以非阻塞和阻塞的方式獲取隊列中的引用對象。