Java內存管理機制
在C++語言中,如果需要動態分配一塊內存,程序員需要負責這塊內存的整個生命週期。從申請分配、到使用、再到最後的釋放。這樣的過程非常靈活,但是卻十分繁瑣,程序員很容易由於疏忽而忘記釋放內存,從而導致內存的泄露。Java語言對內存管理做了自己的優化,這就是垃圾回收機制。Java的幾乎所有內存對象都是在堆內存上分配(基本數據類型除外),然後由GC(garbage collection)負責自動回收不再使用的內存。
上面是Java內存管理機制的基本情況。但是如果僅僅理解到這裏,我們在實際的項目開發中仍然會遇到內存泄漏的問題。也許有人表示懷疑,既然Java的垃圾回收機制能夠自動的回收內存,怎麼還會出現內存泄漏的情況呢?這個問題,我們需要知道GC在什麼時候回收內存對象,什麼樣的內存對象會被GC認爲是“不再使用”的。
Java中對內存對象的訪問,使用的是引用的方式。在Java代碼中我們維護一個內存對象的引用變量,通過這個引用變量的值,我們可以訪問到對應的內存地址中的內存對象空間。在Java程序中,這個引用變量本身既可以存放堆內存中,又可以放在代碼棧的內存中(與基本數據類型相同)。GC線程會從代碼棧中的引用變量開始跟蹤,從而判定哪些內存是正在使用的。如果GC線程通過這種方式,無法跟蹤到某一塊堆內存,那麼GC就認爲這塊內存將不再使用了(因爲代碼中已經無法訪問這塊內存了)。
通過這種有向圖的內存管理方式,當一個內存對象失去了所有的引用之後,GC就可以將其回收。反過來說,如果這個對象還存在引用,那麼它將不會被GC回收,哪怕是Java虛擬機拋出OutOfMemoryError。
Java內存泄露
一般來說內存泄漏有兩種情況。一種情況如在C/C++語言中的,在堆中的分配的內存,在沒有將其釋放掉的時候,就將所有能訪問這塊內存的方式都刪掉(如指針重新賦值);另一種情況則是在內存對象明明已經不需要的時候,還仍然保留着這塊內存和它的訪問方式(引用)。第一種情況,在Java中已經由於垃圾回收機制的引入,得到了很好的解決。所以,Java中的內存泄漏,主要指的是第二種情況。
可能光說概念太抽象了,大家可以看一下這樣的例子:
2 for (int i=1;i<100; i++){
3 Object o=new Object();
4 v.add(o);
5 o=null;
6 }
在這個例子中,代碼棧中存在Vector對象的引用v和Object對象的引用o。在For循環中,我們不斷的生成新的對象,然後將其添加到Vector對象中,之後將o引用置空。問題是當o引用被置空後,如果發生GC,我們創建的Object對象是否能夠被GC回收呢?答案是否定的。因爲,GC在跟蹤代碼棧中的引用時,會發現v引用,而繼續往下跟蹤,就會發現v引用指向的內存空間中又存在指向Object對象的引用。也就是說盡管o引用已經被置空,但是Object對象仍然存在其他的引用,是可以被訪問到的,所以GC無法將其釋放掉。如果在此循環之後,Object對象對程序已經沒有任何作用,那麼我們就認爲此Java程序發生了內存泄漏。
儘管對於C/C++中的內存泄露情況來說,Java內存泄露導致的破壞性小,除了少數情況會出現程序崩潰的情況外,大多數情況下程序仍然能正常運行。但是,在移動設備對於內存和CPU都有較嚴格的限制的情況下,Java的內存溢出會導致程序效率低下、佔用大量不需要的內存等問題。這將導致整個機器性能變差,嚴重的也會引起拋出OutOfMemoryError,導致程序崩潰。
一般情況下內存泄漏的避免
在不涉及複雜數據結構的一般情況下,Java的內存泄露表現爲一個內存對象的生命週期超出了程序需要它的時間長度。我們有時也將其稱爲“對象遊離”。
例如:
2
3 private byte[] content;
4 private File mFile;
5
6 public FileSearch(File file){
7 mFile = file;
8 }
9
10 public boolean hasString(String str){
11 int size = getFileSize(mFile);
12 content = new byte[size];
13 loadFile(mFile, content);
14
15 String s = new String(content);
16 return s.contains(str);
17 }
18 }
在這段代碼中,FileSearch類中有一個函數hasString,用來判斷文檔中是否含有指定的字符串。流程是先將mFile加載到內存中,然後進行判斷。但是,這裏的問題是,將content聲明爲了實例變量,而不是本地變量。於是,在此函數返回之後,內存中仍然存在整個文件的數據。而很明顯,這些數據我們後續是不再需要的,這就造成了內存的無故浪費。
要避免這種情況下的內存泄露,要求我們以C/C++的內存管理思維來管理自己分配的內存。第一,是在聲明對象引用之前,明確內存對象的有效作用域。在一個函數內有效的內存對象,應該聲明爲local變量,與類實例生命週期相同的要聲明爲實例變量……以此類推。第二,在內存對象不再需要時,記得手動將其引用置空。
複雜數據結構中的內存泄露問題
在實際的項目中,我們經常用到一些較爲複雜的數據結構用於緩存程序運行過程中需要的數據信息。有時,由於數據結構過於複雜,或者我們存在一些特殊的需求(例如,在內存允許的情況下,儘可能多的緩存信息來提高程序的運行速度等情況),我們很難對數據結構中數據的生命週期作出明確的界定。這個時候,我們可以使用Java中一種特殊的機制來達到防止內存泄露的目的。
之前我們介紹過,Java的GC機制是建立在跟蹤內存的引用機制上的。而在此之前,我們所使用的引用都只是定義一個“Object o;”這樣形式的。事實上,這只是Java引用機制中的一種默認情況,除此之外,還有其他的一些引用方式。通過使用這些特殊的引用機制,配合GC機制,就可以達到一些我們需要的效果。
Java中的幾種引用方式
Java中有幾種不同的引用方式,它們分別是:強引用、軟引用、弱引用和虛引用。下面,我們首先詳細地瞭解下這幾種引用方式的意義。
強引用
在此之前我們介紹的內容中所使用的引用都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
軟引用(SoftReference)
SoftReference 類的一個典型用途就是用於內存敏感的高速緩存。SoftReference 的原理是:在保持對對象的引用時保證在 JVM 報告內存不足情況之前將清除所有的軟引用。關鍵之處在於,垃圾收集器在運行時可能會(也可能不會)釋放軟可及對象。對象是否被釋放取決於垃圾收集器的算法 以及垃圾收集器運行時可用的內存數量。
弱引用(WeakReference)
WeakReference 類的一個典型用途就是規範化映射(canonicalized mapping)。另外,對於那些生存期相對較長而且重新創建的開銷也不高的對象來說,弱引用也比較有用。關鍵之處在於,垃圾收集器運行時如果碰到了弱可及對象,將釋放 WeakReference 引用的對象。然而,請注意,垃圾收集器可能要運行多次才能找到並釋放弱可及對象。
虛引用(PhantomReference)
PhantomReference 類只能用於跟蹤對被引用對象即將進行的收集。同樣,它還能用於執行 pre-mortem 清除操作。PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因爲它能夠充當通知機制。當垃圾收集器確定了某個對象是虛可及對象時,PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 對象引用的對象已經結束,可供收集了。這使您能夠剛好在對象佔用的內存被回收之前採取行動。Reference與ReferenceQueue的配合使用。
GC、Reference與ReferenceQueue的交互
A、 GC無法刪除存在強引用的對象的內存。
B、 GC發現一個只有軟引用的對象內存,那麼:
① SoftReference對象的referent 域被設置爲null,從而使該對象不再引用heap對象。
② SoftReference引用過的heap對象被聲明爲finalizable。
③ 當 heap 對象的 finalize() 方法被運行而且該對象佔用的內存被釋放,SoftReference 對象就被添加到它的 ReferenceQueue(如果後者存在的話)。
C、 GC發現一個只有弱引用的對象內存,那麼:
① WeakReference對象的referent域被設置爲null,從而使該對象不再引用heap對象。
② WeakReference引用過的heap對象被聲明爲finalizable。
③ 當heap對象的finalize()方法被運行而且該對象佔用的內存被釋放時,WeakReference對象就被添加到它的ReferenceQueue(如果後者存在的話)。
D、 GC發現一個只有虛引用的對象內存,那麼:
① PhantomReference引用過的heap對象被聲明爲finalizable。
② PhantomReference在堆對象被釋放之前就被添加到它的ReferenceQueue。
值得注意的地方有以下幾點:
1、GC在一般情況下不會發現軟引用的內存對象,只有在內存明顯不足的時候纔會發現並釋放軟引用對象的內存。
2、GC對弱引用的發現和釋放也不是立即的,有時需要重複幾次GC,纔會發現並釋放弱引用的內存對象。
3、軟引用和弱引用在添加到ReferenceQueue的時候,其指向真實內存的引用已經被置爲空了,相關的內存也已經被釋放掉了。而虛引用在添加到ReferenceQueue的時候,內存還沒有釋放,仍然可以對其進行訪問。
代碼示例
通過以上的介紹,相信您對Java的引用機制以及幾種引用方式的異同已經有了一定了解。光是概念,可能過於抽象,下面我們通過一個例子來演示如何在代碼中使用Reference機制。
2 ReferenceQueue<String> rq = new ReferenceQueue<String>(); //②
3 WeakReference<String> wf = new WeakReference<String>(str, rq); //③
4 str=null; //④取消"hello"對象的強引用
5 String str1=wf.get(); //⑤假如"hello"對象沒有被回收,str1引用"hello"對象
6 //假如"hello"對象沒有被回收,rq.poll()返回null
7 Reference<? extends String> ref=rq.poll(); //⑥
在以上代碼中,注意⑤⑥兩處地方。假如“hello”對象沒有被回收wf.get()將返回“hello”字符串對象,rq.poll()返回null;而加入“hello”對象已經被回收了,那麼wf.get()返回null,rq.poll()返回Reference對象,但是此Reference對象中已經沒有str對象的引用了(PhantomReference則與WeakReference、SoftReference不同)。
引用機制與複雜數據結構的聯合應用
瞭解了GC機制、引用機制,並配合上ReferenceQueue,我們就可以實現一些防止內存溢出的複雜數據類型。
例如,SoftReference具有構建Cache系統的特質,因此我們可以結合哈希表實現一個簡單的緩存系統。這樣既能保證能夠儘可能多的緩存信息,又可以保證Java虛擬機不會因爲內存泄露而拋出OutOfMemoryError。這種緩存機制特別適合於內存對象生命週期長,且生成內存對象的耗時比較長的情況,例如緩存列表封面圖片等。對於一些生命週期較長,但是生成內存對象開銷不大的情況,使用WeakReference能夠達到更好的內存管理的效果。
附SoftHashmap的源碼一份,相信看過之後,大家會對Reference機制的應用有更深入的理解。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113