垃圾回收機製與引用類型
Java語言的一個重要特性是引入了自動的內存管理機製,這樣一來,開發人員就不需要自己來管理應用中的內存了。C/C++開發人員需要通過malloc/free和new/delete等函數來顯式的分配和釋放內存。這對開發人員提出了比較高的要求,因為這些方法使用不當很容易造成內存訪問錯誤和內存泄露等嚴重問題。一個最常見的問題是 “懸掛引用(dangling references)”,即一個引用或指針所指向的內存區塊已經被錯誤的回收並重新分配給新的對象了,如果程序繼續使用這個引用或指針的話會,必然會造成不可預期的結果。開發人員有可能忘記顯式地調用內存釋放的函數而造成內存泄露。而自動的內存管理則是把管理內存的任務交給編程語言的運行環境來負責。開發人員並不需要關心內存的分配與回收的底層細節。Java平台通過垃圾回收器(GC)來進行自動的內存管理。
一、JAVA的垃圾回收機製
Java的垃圾回收器要負責完成三件任務:
1、分配內存
2、確保被引用的對象的內存不被錯誤回收
3、回收不再被引用的對象的內存空間
垃圾回收是一個複雜而且耗時的操作,如果JVM(Java虛擬機)花費過多的時間在垃圾回收上,則勢必會影響應用的運行性能。一般情況下,當垃圾回收器在進行回收操作的時候,整個應用的執行是被暫時中止(stop-the-world)的。這是因為垃圾回收器需要更新應用中所有對象引用的實際內存地址。
不同的硬件平台所能支持的垃圾回收方式也不同。比如在多CPU的平台上,就可以通過並行的方式來回收垃圾。而單CPU平台則隻能串行進行。不同的應用所期望的垃圾回收方式也會有所不同。服務器端應用可能希望在應用的整個運行時間中,花在垃圾回收上的時間總數越小越好。而對於與用戶交互的應用來說,則可能希望所垃圾回收所帶來的應用停頓的時間間隔越小越好。對於不同的情況,JVM(Java虛擬機)中提供了多種垃圾回收方法以及對應的性能調優參數,應用可以根據需要來進行定製。
Java 垃圾回收機製最基本的做法是分代回收。內存區域被劃分成不同的代,對象根據其存活的時間被保存在對應代的區域中。一般的實現是劃分成3個代:年輕、年老和永久。內存的分配是發生在年輕代中。當一個對象存活時間足夠長的時候,它就會被複製到年老代中。對於不同的代可以使用不同的垃圾回收算法。進行分代回收的出發點是對應用中對象存活時間進行研究之後得出的統計規律。一般來說,一個應用中的大部分對象的存活時間都很短。比如局部變量的存活時間就隻在方法的執行過程中。基於這一點,對於年輕代的垃圾回收算法就可以很有針對性。
年輕代的內存區域被進一步劃分成Eden區和兩個Survivor區(存活區)。Eden區是進行內存分配的地方,是一塊連續的空閑內存區域。在上麵的進行內存分配速度非常快,因為不需要進行可用內存塊的查找。兩個Survivor區中始終有一個是空白的。在進行垃圾回收的時候,Eden區和其中一個非空Survivor區中還存活的對象,根據其存活時間被複製到當前空白的Survivor區或年老代中。經過這一次的複製之後,之前非空的Survivor區中包含了當前還存活的對象,而Eden區和另一個Survivor區中的內容已經不再需要了,隻需要簡單地把這兩個區域清空即可。下一次垃圾回收的時候,這兩個Survivor區的角色就發生了交換。一般來說,年輕代區域較小,而且大部分對象都已經不再存活,因此在其中查找存活對象的效率較高。
而對於年老和永久代的內存區域,則采用的是不同的回收算法,稱為“標記-清除-壓縮(Mark-Sweep-Compact)”法。標記的過程是找出當前還存活的對象,並進行標記;清除則是遍曆整個內存區域,找出其中需要進行回收的區域;而壓縮則是把存活對象的內存移動到整個內存區域的一端,使得另一端是一塊連續的空閑區域,方便進行內存分配和複製。
Java中提供了4種不同的垃圾回收機製。最常用的是串行回收方式,即使用單個CPU回收年輕和年老代的內存。在回收的過程中,應用程序被暫時中止。回收方式使用的是上麵提到的最基本的分代回收。串行回收方式適合於一般的單CPU桌麵平台。如果是多CPU的平台,則適合的是並行回收方式。這種方式在對年輕代進行回收的時候,會使用多個CPU來並行處理,可以提升回收的性能。並發標記-清除回收方式適合於對應用的響應時間要求比較高的情況,即需要減少垃圾回收所帶來的應用暫時中止的時間。這種做法的優點在於可以在應用運行的同時標記存活對象與回收垃圾,而隻需要暫時中止應用比較短的時間。
二、JAVA引用類型
如果一個內存中的對象沒有任何引用的話,就說明這個對象已經不再被使用了,從而可以成為被垃圾回收的候選。不過,由於垃圾回收器的運行時間不確定,所以可被垃圾回收的對象的實際被回收時間是不確定的。對於一個對象來說,隻要有引用的存在,它就會一直存在於內存中。如果這樣的對象越來越多,超出了JVM中的內存總數,JVM就會拋出OutOfMemory錯誤。
雖然垃圾回收的具體運行是由JVM來控製的,但是開發人員仍然可以在一定程度上與垃圾回收器進行交互,其目的在於更好的幫助垃圾回收器管理好應用的內存。
在Java中把對象的引用分為 4 種級別,從而使程序能更加靈活地控製對象的生命周期。這 4 種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
JAVA對象引用類層次結構圖如下圖所示:
圖14-1 JAVA對象引用類層次結構圖
1)強引用類型
在一般的Java程序中,見到最多的就是強引用(strong reference)。如我們最常見的代碼
//變量定義 String str = “Hello Wolrd”; Object obj = new Object(); Date date = new Date(),
在上麵的代碼中str、obj、date都是對象的強引用。對象的強引用可以在程序中到處傳遞。很多情況下,會同時有多個引用指向同一個對象。強引用的存在限製了對象在內存中的存活時間。假如對象A中包含了一個對象B的強引用,那麼一般情況下,對象B的存活時間就不會短於對象A。如果對象A沒有顯式的把對象B的引用設為null的話,就隻有當對象A被垃圾回收之後,對象B才不再有引用指向它,才可能獲得被垃圾回收的機會。
2)軟引用類型
軟引用(soft reference)在強度上弱於強引用,通過SoftReference類來表示。它的作用是告訴垃圾回收器,程序中的哪些對象並不那麼重要,當內存不足的時候是可以被暫時回收的。當JVM中的內存不足的時候,垃圾回收器會釋放那些隻被軟引用所指向的對象。如果全部釋放完這些對象之後,內存還不足,才會拋出Out Of Memory錯誤。
軟引用非常適合於創建緩存。當係統內存不足的時候,緩存中的內容是可以被釋放的。比如考慮一個圖像編輯器的程序,該程序會把圖像文件的全部內容都讀取到內存中,以方便進行處理,而用戶也可以同時打開多個文件。當同時打開的文件過多的時候,就可能造成內存不足。如果使用軟引用來指向圖像文件內容的話,垃圾回收器就可以在必要的時候回收掉這些內存。
從下麵的Java代碼中可以看到軟引用類型的使用方法。
public class BitmapCache { private String url;//圖片URL private SoftReference<Bitmap> softRef;// //軟引用-隻有當係統內存不足的時候才去釋放 public BitmapCache (String url) { this. url = url; softRef = new SoftReference< Bitmap >(null); } private Bitmap loadRemoteBitmap() { final DefaultHttpClient client = new DefaultHttpClient(); final HttpGet getRequest = new HttpGet(url); HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); final HttpEntity entity = response.getEntity(); InputStream inputStream = entity.getContent(); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); OutputStream outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); copy(inputStream, outputStream); outputStream.flush(); final byte[] data = dataStream.toByteArray(); final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); return bitmap; } public Bitmap get Bitmap () { Bitmap bitmap = softRef.get(); if (bitmap == null) {//係統內存不足的時,圖片已經被釋放需要重新加載網絡圖片 bitmap = loadRemoteBitmap (); softRef = new SoftReference< Bitmap >( bitmap); } return bitmap; } }
在使用上麵程序的時候,由於軟引用所指向的對象可能被回收掉,在通過get方法來獲取軟引用所實際指向的對象的時候,總是要檢查該對象是否還存活。
3)弱引用類型
弱引用(weak reference)在強度上弱於軟引用,通過WeakReference類來表示。它的作用是引用一個對象,但是並不阻止該對象被回收。如果使用一個強引用的話,隻要該引用存在,那麼被引用的對象是不能被回收的,弱引用則沒有這個問題。在垃圾回收器運行的時候,如果一個對象的所有引用都是弱引用的話,該對象會被回收。弱引用的作用在於解決強引用所帶來的對象之間在存活時間上的耦合關係。
弱引用最常見的用處是在集合類中,尤其在哈希表中。哈希表的接口允許使用任何Java對象作為鍵(Key)。當一個鍵值對(Key-Value)被放入到哈希表中之後,哈希表對象本身就有了對這些鍵和值對象的引用。如果這種引用是強引用的話,那麼隻要哈希表對象本身還存活,其中所包含的鍵和值對象是不會被回收的。如果某個存活時間很長的哈希表中包含的鍵值對很多,最終就有可能消耗掉JVM中全部的內存。對於這種情況的解決辦法就是使用弱引用來引用這些對象,這樣哈希表中的鍵和值對象都能被垃圾回收器及時回收。在Java中可以使用WeakHashMap類來滿足這一常見需求。
4)虛引用類型
在介紹虛引用之前,先要了解一下Java提供的對象終止化機製(finalization)。
在Object類裏麵有個finalize方法,其設計的初衷是在一個對象被真正回收之前,可以用來執行一些清理的工作。因為Java並沒有提供類似C++的析構函數一樣的機製,隻是簡單地通過 finalize方法來實現。但是問題在於垃圾回收器的運行時間是不固定的,所以這些清理工作的實際運行時間也是不能預知的。
使用虛引用(phantom reference)可以解決這個問題。在創建虛引用PhantomReference的時候必須要指定一個引用隊列。當一個對象的finalize方法已經被調用了之後,這個對象的虛引用會被加入到隊列中。通過檢查該隊列裏麵的內容就知道一個對象是不是已經準備要被回收了。
虛引用及其隊列的使用情況並不多見,主要用來實現比較精細的內存使用控製,這對於移動設備來說是很有意義的。程序可以在確定一個對象要被回收之後,再申請內存創建新的對象。通過這種方式可以使得程序所消耗的內存維持在一個相對較低的數量。
比如下麵的Java代碼給出了一個緩衝區的實現示例。
//虛引用實例代碼 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) { //校驗size是否合法 if (size <= 0) { // size不合法,拋出異常 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方法會阻塞直到新的虛引用被加入到隊列中。需要注意的是,這種做法會導致垃圾回收器被運行的次數過多,可能會造成程序的吞吐量過低。
5)引用隊列
在有些情況下,程序會需要在一個對象的可達到性發生變化的時候得到通知。比如某個對象的強引用都已經不存在了,隻剩下軟引用或是弱引用。但是還需要對引用本身做一些的處理。典型的情景是在哈希表中,引用對象是作為WeakHashMap中的鍵對象的,當其引用的實際對象被垃圾回收之後,就需要把該鍵值對從哈希表中刪除。有了引用隊列(ReferenceQueue),就可以方便的獲取到這些弱引用對象,將它們從表中刪除。在軟引用和弱引用對象被添加到隊列之前,其對實際對象的引用會被自動清空。通過引用隊列的poll/remove方法就可以分別以非阻塞和阻塞的方式獲取隊列中的引用對象。
最後更新:2017-04-02 15:32:47