對於某些編程語言,內存管理的工做須要開發人員處理,好比C/C++。以C++爲例,程序經過new操做符建立新對象以後,就會分配相應的內存資源,當程序再也不須要這些對象時,須要在代碼中將其顯式釋放,通常經過delete操做符完成。內存分配和釋放的正確性,由開發人員來保證。開發過程當中可能出現的與內存釋放相關的問題主要有:懸掛引用(野指針)和內存泄露。java
C/C++中常說的野指針。懸掛引用指的是對某個對象引用實際上指向一個錯誤的內存地址。好比,算法
1 int * a = new int [10]; 2 int * b = a; // b 和 a 指向同一塊內存區域 3 delete [] a; // 釋放 a 指向的那塊內存區域 4 printf("b[0]:%d", b[0]); // 打印出無效的值 5 b[0] = 2; // 再次引用已經釋放的內存區域,結果沒法預料。
上述代碼中指針b就是一個野指針。b和a指向同一塊內存控件,"delete a;"語句已經將該空間釋放,對b[0]進行打印會出現莫名的值,而非初始化的0,而"b[0] = 2;"試圖再次引用該內存區域將會出現不可預知的錯誤。編程
某些對象所佔用的內存沒有被釋放,又沒有對象引用指向這些內存。這樣就致使這部份內存對程序來講既不可用,又沒法被釋放,出現內存泄露。C/C++中的內存泄露指的是內存不可達。緩存
由於顯式內存管理比較容易出錯,所以很多語言引入了內存自動管理機制,典型的表明爲Java語言。Java提供了垃圾回收器(GC)來自動回收程序以後再也不使用的內存。GC不只負責內存的回收,還負責內存的分配。編程語言
當Java程序運行時,GC也同時運行在一個單獨的線程中,它會根據虛擬機當前的內存狀態決定何時進行垃圾回收工做。而GC的執行回收的具體時間和頻率沒法預估,取決於GC的實現算法。程序能夠經過System.gc方法建議GC當即執行垃圾回收,不過在這種狀況下GC也可能選擇不執行回收。ide
某些狀況,當系統可用內存愈來愈少,而GC又沒法找到足夠可用的空閒內存時,建立新對象的操做就會拋出OOM(OutOfMemory)錯誤,致使虛擬機退出。好比,一個圖像處理程序可能同時打開了多個圖像文件進行編輯,而同一時刻只有一張圖片處於編輯狀態,當同時打開多張圖片時,程序佔用的內存空間就會變大,而GC又沒法回收這些處於活動的對象所佔用的內存,使得可用內存愈來愈少。以後建立新對象的操做就可能致使OOM錯誤。spa
對於上述狀況,咱們的Java程序須要經過一種方式把對象在內存需求方面的特徵告訴GC,GC能夠根據對象特徵進行更好地回收。這種方式就是經過Java中對象的引用類型來實現。程序運行中,對於同一個對象,可能存在多個指向它的引用,若是再也不有引用指向該對象,那麼這個對象就會成爲GC的候選目標。Java中存在不一樣的對象引用類型,不一樣類型的引用對GC的含義是不一樣的。分別爲強引用、軟引用、弱引用以及虛引用。線程
最多見的引用類型,也是默認的引用類型。使用new操做符建立一個的新對象,並將其複製給一個變量的時候,這個變量就成爲指向該對象的一個強引用。例如:指針
"Object a = new Object();", 變量a就是一個強引用,指向一個new操做符建立的Object類型的對象。對於強引用的對象,GC是不會將其做爲垃圾收集的候選目標。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足的問題。code
強引用會阻止GC對其進行內存釋放,若是某個對象以後再也不使用,而程序中卻一直保存着其強引用,就會致使GC沒法對這塊內存進行釋放,形成了「Java中所說的內存泄露」。這種內存泄露和以前提到的C/C++的內存泄露意思不一樣,C/C++中內存泄露是指某些內存不可達,而又沒法釋放。Java中不可達的內存直接就被GC回收了,Java中的內存泄露本質上是這些內存是可達的,而以後程序不想繼續使用它卻沒法被GC回收,形成了「泄露」。
Java中的內存泄露主要分兩種狀況:
第一種內存泄露的例子以下:
1 public class LeakedQueue<T> { 2 private List<T> backendList = new ArrayList<T>(); 3 private int topIndex = 0; 4 5 public void enqueue(T value) { 6 backendList.add(value); 7 } 8 9 public T dequeue() { 10 if (topIndex < backendList.size()) { 11 T result = backendList.get(topIndex); 12 topIndex++; 13 return result; 14 } 15 return null; 16 } 17 }
出對方法會形成內存泄露。對於dequeue的對象,backendList沒有將其刪除,而依然保存了該對象的強引用,所以該對象沒法被GC回收。屢次執行enqueue和dequeue後將會使虛擬機可用內存愈來愈少,致使OOM錯誤。
第二種內存泄露狀況一般發生在基於內存實現的緩存的時候,例子以下:
1 public class Calculator { 2 private Map<String, Object> cache = new HashMap<String, Object>(); 3 4 public Object calculate(String expr) { 5 if (cache.containsKey(expr)) { 6 return cache.get(expr); 7 } 8 Object result = doCalculate(expr); 9 cache.put(expr, result); 10 return result; 11 } 12 13 private Object doCalculate(String expr) { 14 return new Object(); 15 } 16 }
緩存的存活時間和Calculator同樣長,只要Calculator沒法被回收,其中所包含的計算結構對象也沒法被回收,不斷執行新的calculate,將會致使內部緩存愈來愈大,可用內存愈來愈少。
當強引用存在的時候,所指向的對象沒法被GC回收,爲了加強程序與GC的交互能力,JDK 1.2引入了java.lang.ref包,提供了三種新的引用類型,分別是軟引用,弱引用和虛引用。這些引用類型除了能夠引用對象以外,還能夠在不一樣程度上影響GC對被引用對象的處理行爲。
軟引用強度上弱於強引用,用SoftReference類來表示。若是一個對象不是強引用可達,同時能夠經過軟引用來訪問,則該對象時軟引用可達的。軟引用傳遞給GC的信息是:只有你還有足夠的內存,就不要回收軟引用指向的對象。GC會在拋出OOM錯誤發生以前,回收掉軟引用指向的對象。
使用軟引用:
1 Object aObject = new Object(); 2 SoftReference<Object> ref = new SoftReference<Object>(aObject); 3 aObject = null; // 必需要釋放掉強引用,不然無心義。由於只要強引用存在,GC就不會進行回收。 4 5 Object bObject = ref.get(); // 經過get方法獲取引用的對象,獲取以後須要判斷是否爲NULL,由於可能已經被GC回收 6 if (bObject != null) { 7 // your opr 8 }
弱引用在強度上弱於軟引用,用WeakReference表示。弱引用傳遞給GC的信息是:在判斷一個對象是否存活時,能夠不考慮弱引用的存在。只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程,所以不必定會很快發現那些只具備弱引用的對象。
虛引用是強度最弱的一種引用,用PhantomReference類表示。虛引用的主要目的是在一個對象所佔的內存被實際回收以前獲得通知,從而能夠進行一些相關的清理工做。虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收器回收。虛引用在使用上與前兩種引用有很大不一樣:首先,虛引用建立時必須提供一個引用隊列做爲參數;其次,虛引用對象的get方法老是返回null,所以沒法經過虛引用獲取被引用的對象。
程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。
引用隊列的主要做用是做爲一個通知機制。當對象的可達狀態發生變化時,若是程序但願獲得通知,可使用引用隊列。當從引用隊列中獲取了引用對象後,不可能再獲取所指向的具體對象。由於,對於軟引用和弱引用,在被放入隊列以前,它們的引用關係就已經被清除。而虛引用的get方法老是返回null。