這篇文章整理自一位外國大神的英文博客彤枢,我在保存文章的結構下,增加了一些自己的見解茬贵,并做了一個文章的腦圖摘完。原文鏈接為 http://www.kdgregory.com/index.php?page=java.refobj
腦圖如下
基礎
Java堆和對象生命周期
在 Java 中,隨著函數的調用羹令,局部變量和函數會被壓入棧幀鲤屡,而 new 操作符生成出來的實際對象保存在堆中,當然福侈,如果這時堆中沒有合適足夠的空間生成新對象酒来,在報出 OutOfMemoryError
之前,就會嘗試進行一次垃圾收集來獲取空間肪凛。
垃圾收集
Java 語言給我們提供了 new
操作符來在堆中分配一塊內存堰汉,但是卻沒有給我們提供一個delete
操作符來釋放這些空間辽社,如果僅僅是這樣,那么我們的堆內存空間很快就會占滿翘鸭,程序久無法繼續(xù)執(zhí)行了滴铅。
幸運的是 Java 給我們提供了垃圾收集器。在我們 new 一個對象時就乓,如果堆內存空間不足汉匙,調用 new 操作符的線程就會被掛起,等待垃圾收集器掃描一遍堆內存并釋放空間生蚁,如果收集后仍然沒有足夠的空間噩翠,就會報出 OOM 了。
標記-清除算法
標記清除算法可以概括為:所有不可達的對象都是垃圾邦投,并且可以被收集清除伤锚。
標記清除算法有如下步驟:
步驟一:標記
垃圾收集器從根引用開始,遍歷對象關系圖并把遍歷過的對象標記為可達對象尼摹。
步驟二:清除
所有在步驟一中沒有被標記到的對象见芹,如果有定義 finalizer,則會被加入到 finalization queue 中執(zhí)行 finalize 方法蠢涝,否則會被清除玄呛。
步驟三:壓縮(可選)
有些垃圾收集器會有第三個步驟:壓縮整理堆內存,即把第二步執(zhí)行結束后和二,零碎的堆內存重新對齊徘铝,整理出大片的連續(xù)堆內存空間。
比如惯吕,在1.6 和 1.7 server 模式下的 Hotspot JVM惕它,就會將年輕代的空間壓縮整理,但是不會壓縮整理老年代的空間废登。
Finalizer
雖然 Java 替我們提供了垃圾收集機制去釋放堆內存淹魄,但是內存不是我們唯一要清理的資源。比如堡距,FileOutputStream
在不可達之后甲锡,被垃圾收集器收集之前,應該釋放它關聯的文件和系統(tǒng)的連接羽戒、把緩沖區(qū)的數據刷入文件等缤沦。為此,Java 為我們提供了 Finalizer 機制易稠,我們只要實現 finalize() 方法即可缸废。
雖然 Finalizer 看起來簡單好用,但是我們不應該依賴它,因為如果垃圾收集一直不執(zhí)行企量,那么它將一直不被調用测萎。如果有過多的 Finalizer 拖住了內存空間的清理,那么也可能導致不能及時釋放出足夠的空間而出現 OOM梁钾。
Java 對象生命周期(沒有 Reference 的情況下)
創(chuàng)建---->構造---->使用---->不可達---->Finalizer
引用關系
Reference 對象
三個生命周期中的新狀態(tài)
從 JDK 1.2 起绳泉,Java 提供了三個新的狀態(tài)在 Java 生命周期中:分別是軟可達(softly-reachable)、弱可達(weakly-reachable)姆泻、虛幻可達(phantom-reachable)零酪。這些狀態(tài)都是應用在對象滿足垃圾收集狀態(tài)時,換句話說拇勃,就是這個對象已經沒有任何強引用了四苇。
-
軟可達: 當對象被一個
SoftReference
關聯,并且沒有強引用時方咆,這個對象就進入了軟可達狀態(tài)月腋。在這個狀態(tài)下,垃圾收集器會嘗試不去收集這個對象瓣赂,直到如果不收集這個對象就會引發(fā) OOM 才會嘗試收集它榆骚。 -
弱可達: 當對象被一個
WeakReference
關聯,并且沒有強或者軟引用時煌集,這個對象就進入了弱可達狀態(tài)妓肢。在這個狀態(tài)下,垃圾收集器可以自由收集這個對象而不受約束苫纤。不過實際上碉钠,只有在 full collect 時會收集清除弱可達對象,小收集是不會清除的卷拘。 -
虛幻可達: 當對象被一個
PhantomReference
關聯時喊废,并且對象已經被垃圾收集器盯上且 finalize() 方法已經執(zhí)行時,這個對象就進入了虛幻可達狀態(tài)栗弟。換句話說污筷,已經沒有任何方式可以挽回這個對象了~
這里需要注意的兩個點是:
- 對象可以跳過其中某些生命周期。比如沒有軟引用乍赫,只有弱引用颓屑。那么對象可以直接從強可達進入弱可達狀態(tài)。
- 只有極少對象需要用到這些引用關系耿焊。
引用關系和被引用對象
Reference 引用關系是我們程序和具體對象之間的一個中間層,其中被引用的對象是在 Reference 構造時指定的遍搞,并且不可修改罗侯。下面是一個例子:
SoftReference<List<Foo>> ref = new SoftRerence<>(new LinkedList<Foo>);
List<Foo> list = ref.get();
if (list != null){
list.add(foo);
}else {
// somthing else
}
其中要注意的點是:
- 每次使用對象前必須確認對象是否已經被清理(null)
- 必須先拿到對象的強引用再使用對象。不然直接使用
ref.get().add(foo)
溪猿,如果這時在執(zhí)行到 ref.get() 時觸發(fā)了一次垃圾收集钩杰,將會報 NPE纫塌。 - 比如給這個 Reference 指定一個強引用。如果這個引用關系被垃圾收集清理了讲弄。那我們講這么多都沒用了……
軟引用
在 JDK 文檔中講了措左,軟引用關系適合用于內存敏感的緩存:每個被緩存的對象通過一個 SoftReference 連接,然后 JVM 會在不需要這部分被引用對象的空間時避除,不去清理它怎披,在內存空間不足時再清理。也因此瓶摆,對于正在使用的緩存對象凉逛,我們應該加上一個強引用指向被引用對象,防止它被清理群井。當然状飞,如果要使用的對象已經被清理了,我們就刷新一下緩存再加它進去即可书斜。
需要注意的是诬辈,不建議緩存很小的對象,應該緩存大文件荐吉、大對象焙糟、層層嵌套的對象圖的根對象之類的。因為稍坯,如果緩存小文件酬荞,那么需要清理很多很多對象才能釋放出看起來有起色的內存空間,并且這個引用關系也會占用很多空間瞧哟。
使用軟引用來觸發(fā)循環(huán)的終止
這時軟引用的一種典型的用途混巧,可以在循環(huán)繼續(xù)運行時會觸發(fā) OOM 的情況下,終止循環(huán)勤揩,避免 OOM 的出現咧党。
來看下面一段代碼:
public List<Object> getBigObjectListByIdList(List<String> ids){
List<Object> list = new LinkedList<>();
for (String id : ids){
list.add(getBigObjectFromDisk(id));
}
return list;
}
顯然如果這時內存空間不足,經過垃圾收集后仍然不夠的話陨亡。程序將會發(fā)出 OOM 然后崩潰傍衡。如果這時我們給 list 對象套上一層軟引用,并判斷 list 對象的狀態(tài)是否為 null 來決定是否終止循環(huán)负蠕。那么當內存不足時蛙埂,list 將會清理,循環(huán)將終止遮糖,OOM 就可以被避免绣的,程序的魯棒性就能得到增強。當然,依舊提供代碼示例:
public List<Object> getBigObjectListByIdList(List<String> ids){
SoftReference<List<Object>> ref = new SoftReference<>(new LinkedList<>());
for (String id : ids){
List<Object> list = ref.get();
if (list == null)
return null;
else
list.add(getBigObjectFromDisk(id));
list = null;
}
return list;
}
需要注意的是屡江,我在循環(huán)末尾把 list 顯式聲明為 null芭概,因為這里避免了一種特殊情況,雖然我們在循環(huán)結束時失去 list 這個對象惩嘉,但是垃圾收集器可能還沒發(fā)現它已經是不可達狀態(tài)罢洲,因為 list 的引用還存在 JVM 的棧中,是處于一種不明顯文黎、不易被察覺的強引用狀態(tài)惹苗。
軟引用不是銀彈
雖然軟引用可以幫我們避免很多內存溢出的情況,但是卻不能避免所有情況臊诊。問題在于:當我們實際使用一個軟引用來連接對象時鸽粉,比如上面的 getBigObjectListByIdList(List<String> ids)
函數,當我們要添加一行新數據到結果里抓艳,我們必須先拿到被引用對象 list 的強引用触机。在我們拿到 list 強引用的這段時間,我們就處在 OOM 的風險中玷或。
這樣看來儡首,使用軟引用作為循環(huán)的終止,只是最小化了我們觸發(fā) OOM 的風險偏友,并沒有完全解決了 OOM 的問題蔬胯。
弱引用
弱引用,如同它的名字一樣位他,在 gc 時它不會做任何反抗氛濒,只要被引用對象沒有存在強引用關系,即使保留了弱引用關系鹅髓,仍會被清理舞竿。
弱引用關系,存在肯定不會一無是處啦窿冯。它也有適合的應用場景:
- 連接那些沒有天生存在關聯的對象
- 通過一個調度 map骗奖,來減少重復數據。(緩存)
連接那些沒有天生存在關聯的對象
比如 ObjectOutputStream 使用了一個 WeakClassKey 來保存最近輸出的對象的 ObjectStreamClass醒串。避免反復對同一個Class創(chuàng)建ObjectStreamClass對象执桌。
從被序列化的對象的角度來看,它跟 ObjectOutputStream 沒有天生的關聯芜赌,從 ObjectOutputStream 的角度來看仰挣,它跟被序列化的對象的 ObjectStreamClass 只是存在使用時要用到的關系,也不是天然有關聯的缠沈。
假設我們寫了一個程序椎木,這個程序直接強引用 ObjectStreamClass 作為 socket 中發(fā)送消息的協(xié)議殴瘦,那么這里就存在一個問題:每個消息一瞬間就發(fā)送完了叉钥,但是消息對象的 ObjectStreamClass 仍然存在內存中一直占有這部分資源,那么這部分內存就廢了抗斤,慢慢程序的內存也會被耗盡禽篱。(除非我們顯式釋放掉這部分內存)
這樣看來畜伐,弱引用提供了這樣一種方式去維持對象的引用關系:當對象正在使用被引用對象時,就顯式持有一個被引用對象的強引用躺率,當使用完被引用對象后玛界,就釋放掉強引用關系,只留下弱引用關系悼吱。這個弱引用關系會維持住跟被引用對象的連接慎框,以期待下次程序再次調用到被引用對象時,將其取出后添,或者直到被引用對象被垃圾收集器清理笨枯。
通過一個調度 map 來減少重復數據
這個功能跟 String.intern()
極其相似,假設我們手動實現一個 String.intern() 方法遇西,就可以通過一個 WeakHashMap 和 WeakReference 配合實現:
private Map<String,WeakReference<String>> _map
= new WeakHashMap<String,WeakReference<String>>();
public synchronized String intern(String str)
{
WeakReference<String> ref = _map.get(str);
String s2 = (ref != null) ? ref.get() : null;
if (s2 != null)
return s2;
_map.put(str, new WeakReference(str));
return str;
}
當存在大量的相同的 String 對象時馅精,這個做法就可以節(jié)省大量的內存,使它們都引用到同一個 String 對象的地址粱檀;當一個 String 不再被使用時洲敢,就可以被垃圾收集器自由清理掉,不再占用空間茄蚯。推廣到其他對象压彭,也可以用這種方法來減少重復對象。這其實也是一種緩存渗常。
引用隊列 Reference Quences
當我們在創(chuàng)建一個引用關系時壮不,把這個引用關系關聯到一個隊列,并且這個引用在對象被清理時被入隊凳谦。當我們們要尋找哪個對象被清理掉時忆畅,就來隊列中尋找。那這就是引用隊列的作用了尸执。
下面提供一個使用 Reference Queue 的例子
public static void main(String[] argv) throws Exception
{
Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>();
ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
for (int ii = 0 ; ii < 1000 ; ii++)
{
WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue);
System.err.println(ii + ": created " + ref);
refs.add(ref);
Reference<? extends byte[]> r2;
while ((r2 = queue.poll()) != null)
{
System.err.println("cleared " + r2);
refs.remove(r2);
}
}
}
通過這個例子我們可以看出引用隊列的兩個要點:
- 一旦入隊了家凯,那么這個對象就已經被清理了,回不來了如失。
- 引用關系知道引用隊列的存在绊诲,引用隊列不知道引用關系的存在。所以我們必須持有一個引用關系的強引用褪贵。同時我們又要在做完我們對入隊對象的操作后掂之,清理掉這個引用關系的強引用抗俄,否則,這就會觸發(fā)內存泄漏了世舰。
虛幻引用
虛幻引用不同于軟引用和弱引用的是动雹,不能通過虛幻引用關系獲得被引用對象(它的 get() 方法始終返回 null)。所以跟压,虛幻引用唯一的作用應該就是告訴程序被引用對象被垃圾回收了(通過 ReferenceQueue)胰蝠。
雖然虛幻引用表面上看起來沒什么用,但是它可以在資源回收方面做得比 Finalizer 好一些震蒋。(但并沒有完全解決 Finalizer 的問題)
Finalizer 存在的問題
- finalize() 方法可能會一直沒有被調用
如果我們一直沒有耗盡可用的內存茸塞,那么垃圾回收可能會一直不被執(zhí)行,finalize() 也就會一直不被調用查剖。
雖然有辦法在程序退出之前通知 JVM 調用 Finalizer钾虐,但這個方法不太可靠,而且還可能和其他 JVM 退出時的 hook 沖突笋庄。
- Finalizer 機制制造了另一個強引用
在垃圾回收時效扫,如果一個對象即將被清理,但是它實現了 FInalizer无切,那它就暫時不會被立刻清理荡短,而是加入到另個獨立于垃圾收集的線程去執(zhí)行 Finalizer。假如我們所有對象都實現了 Finalizer哆键,那那么垃圾收集將沒有任何成果掘托,OOM 也將出現。
要多說一點的是籍嘹,不建議使用 finalizer 去釋放資源闪盔,但也不建議使用虛幻引用去清理資源。最好還是手動在 try/catch/finally 或 try-resources 去釋放資源辱士。
關于虛幻引用不得不知道的知識
虛幻引用允許程序去清理那些不再被使用的對象泪掀,因此程序可以借此清理已經不在內存中的資源。不像 finalizers颂碘,我們使用虛幻引用來清理對象時异赫,對象已經不再內存了。
虛幻引用還有一點跟 Finalizer 不一樣的是头岔,清理是在程序調用的時候進行的塔拳,而不是在垃圾收集的時候觸發(fā)的。我們可以根據我們需要峡竣,開一個或者多個線程來清理對象靠抑。一個可選的方式就是,我們通過一個對象工廠來生產我們需要的資源适掰,然后工廠在生產一個新的資源出來之前颂碧,先進行一次清理荠列,把已經被垃圾收集的資源做一次清理。
理解虛幻引用最關鍵的點就是:我們不能通過這個引用關系 reference 去訪問對象: get() 一直返回 null载城,即使是這個被引用的對象是強可達的肌似。這也意味著我們這個虛幻引用不能幫我們拿到被引用對象,我們也無法通過虛幻引用知道對象是否被清理诉瓦。所以我們必須自己另外對被引用對象做一個強引用保存起來锈嫩,并用一個引用隊列 ReferenceQueue 來標記那些已經被垃圾收集的對象。
下圖是虛幻引用典型的使用方式垦搬,看不懂的可以配合后面的虛幻引用實現連接池的例子來理解。
使用虛幻引用實現一個連接池
數據庫連接是應用中最寶貴的資源之一:它需要花一定的時間來建立連接艳汽,并且數據庫服務器會嚴格限制并發(fā)連接的數量猴贰。也因此,程序員們應該非常謹慎地使用數據庫連接河狐。但還是有時會有為了查詢打開連接米绕,然后忘記手動清理或者忘記在 finally 塊中清理。
比起在應用中直接使用數據庫連接馋艺,大多數應用還是會選擇使用數據庫連接池來管理連接:這個連接池會維持一定地數據庫連接栅干,并且在程序需要使用到數據庫連接的使用從提供可用的連接【桁簦可靠的連接池會提供幾種功能來防止連接泄漏碱鳞,包括超時(連接查詢太長時間),還有從垃圾回收中恢復可用的連接踱蛀。
后面這個功能窿给,就可以用虛幻引用來實現了。為了達到目的率拒,連接池提供的連接 Connection 必須在真實的數據庫連接上做一層包裝崩泡。這樣做的好處是,被包裝的連接對象可以被系統(tǒng)垃圾回收猬膨,但是底層真實的數據庫連接仍會保留下來繼續(xù)被后續(xù)使用角撞。這樣看來,數據庫連接池通過虛幻引用來關聯包裝的連接勃痴,并且在虛幻引用進入引用隊列時谒所,回收真實的連接到連接池中。
這個池還有一個點要關注召耘,那就是 PooledConnection 類百炬,代碼在下面。如同上面說的污它,這是一個包裝過的類剖踊,它將請求委派給真正的連接庶弃。其中,我用了動態(tài)代理來實現這個類德澈。每個 Java 版本的 JDBC 接口都在改進歇攻,也因此,如果是根據某個 JDK 寫出來的代碼梆造,那么前一個版本的 JDK 或者后面版本的 JDK 都可能跑不動下面的連接池代碼缴守。這里使用了動態(tài)代理就解決了這個問題,而且也使得代碼簡潔了一些镇辉。
public class PooledConnection
implements InvocationHandler
{
private ConnectionPool _pool;
private Connection _cxt;
public PooledConnection(ConnectionPool pool, Connection cxt)
{
_pool = pool;
_cxt = cxt;
}
private Connection getConnection()
{
try
{
if ((_cxt == null) || _cxt.isClosed())
throw new RuntimeException("Connection is closed");
}
catch (SQLException ex)
{
throw new RuntimeException("unable to determine if underlying connection is open", ex);
}
return _cxt;
}
public static Connection newInstance(ConnectionPool pool, Connection cxt)
{
return (Connection)Proxy.newProxyInstance(
PooledConnection.class.getClassLoader(),
new Class[] { Connection.class },
new PooledConnection(pool, cxt));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
// if calling close() or isClosed(), invoke our implementation
// otherwise, invoke the passed method on the delegate
}
private void close() throws SQLException
{
if (_cxt != null)
{
_pool.releaseConnection(_cxt);
_cxt = null;
}
}
private boolean isClosed() throws SQLException
{
return (_cxt == null) || (_cxt.isClosed());
}
}
要關注的最重要的地方是屡穗,PooledConnection 關聯了底層的數據庫連接還有我們的連接池。后一個點用來讓程序關閉包裝的連接:我們通知連接池我們已經用完了連接忽肛,然后連接池就可以回收底層真正的連接來重用村砂。
還要提及一下 getConnection() 方法,它檢查了一種特殊情況:程序是否嘗試使用一個已經關閉的連接屹逛。如果沒有這個檢查础废,然后直接使用一個已經重新分配給其他地方使用的連接,那么會造成相當惡劣的結果罕模∑老伲總結起來就是, close() 顯示地關閉包裝的連接淑掌,getConnection() 檢查連接是否被關閉的特殊情況蒿讥,然后動態(tài)代理委派請求給真實的底層連接。
接下來看看連接池的代碼
private Queue<Connection> _pool = new LinkedList<Connection>();
private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>();
private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>();
private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();
我們構建完底層可用的連接后將它存儲在 _pool锋拖,然后使用一個引用隊列來標記那些已經被關閉的包裝連接诈悍。最后,我們使用兩個 Map 來構成底層連接和包裝連接的虛幻引用的雙向 Map兽埃,用來釋放已經用完的連接侥钳。
如同我們上面說的,真實的底層數據庫連接會被包裝起來柄错,這里我們用了 wrapConnection() 方法來做這件事舷夺,在這個方法里我們還創(chuàng)建了虛幻引用,并做了連接-引用雙向映射售貌。
private synchronized Connection wrapConnection(Connection cxt)
{
Connection wrapped = PooledConnection.newInstance(this, cxt);
PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue);
_cxt2Ref.put(cxt, ref);
_ref2Cxt.put(ref, cxt);
System.err.println("Acquired connection " + cxt );
return wrapped;
}
跟 wrapConnection 相反的是 releaseConnection()给猾,這個方法有兩種處理情況:一種是連接被顯式關閉釋放。
synchronized void releaseConnection(Connection cxt)
{
Object ref = _cxt2Ref.remove(cxt);
_ref2Cxt.remove(ref);
_pool.offer(cxt);
System.err.println("Released connection " + cxt);
}
另一種是連接沒有被手動釋放颂跨,而是被垃圾回收后敢伸,我們通過相應的虛幻引用,來解放底層連接恒削。
private synchronized void releaseConnection(Reference<?> ref)
{
Connection cxt = _ref2Cxt.remove(ref);
if (cxt != null)
releaseConnection(cxt);
}
另外池颈,有一種邊緣情況我們要考慮的是:如果我們程序并發(fā)調用了 getConncetion() 和 close() 會怎么樣尾序?這也是為什么我在上面的 releaseConnection() 中添加了一個 synchronized 關鍵字,接下來我們再改造下 getConnection() 方法躯砰,加上 synchronized每币,就避免了這種邊緣情況。
public Connection getConnection() throws SQLException
{
while (true)
{
synchronized (this)
{
if (_pool.size() > 0)
return wrapConnection(_pool.remove());
}
tryWaitingForGarbageCollector();
}
}
可以想到琢歇,理想的情況是我們每次請求 getConnection() 都會返回一個可用的連接兰怠,但是我們必須考慮沒有現成的可用連接的情況,這里我們就用了 tryWaitingForGarbageCollector() 方法來檢查有沒有廢棄的連接沒有被顯式清理掉李茫,并解放底層的連接揭保。
private void tryWaitingForGarbageCollector()
{
try
{
Reference<?> ref = _refQueue.remove(100);
if (ref != null)
releaseConnection(ref);
}
catch (InterruptedException ignored)
{
// we have to catch this exception, but it provides no information here
// a production-quality pool might use it as part of an orderly shutdown
}
}
相關代碼我已經整理到了 github 上:https://github.com/wean2016/ConnectionPool
虛幻引用存在的問題
如同 Finalizer,虛幻引用也存在如果垃圾回收一直不執(zhí)行魄宏,那么它相關的代碼就一直不會運行的問題掖举。如果在上面的例子中,我們初始化了 5 個連接娜庇,并且一直向連接池申請連接,那么可用連接很快就會耗盡方篮,垃圾回收不會執(zhí)行名秀,我們將一直陷入等待。
解決這個問題最簡單的方法是藕溅,在 tryWaitingForGarbageCollector 手動調用 System.gc()匕得。這個解決方案也同樣適用于 Finalizer。
但這不意味著我們可以只關注 Finalizer 而忽視虛幻引用巾表。實際上汁掠,如果這個連接池用 Finalizer 來處理,我們需要關閉連接池的話集币,在 Fianlizer 中我們要顯式手動關閉連接池和相關連接考阱,代碼相當長。而使用虛幻引用來做這件事鞠苟,那就很簡潔了乞榨,只要關聯一下虛幻引用就可以在合適的時候清理掉了。
一個最后的思考:有時候我們也許只是需要更大的內存
有時候引用對象確實是我們管理內存相當有用的工具当娱,但是它們并不是萬能的吃既。如果我們要維持一個超大的對象連接圖,但是我們只有極少內存跨细,那么我們再怎么秀鹦倚,也秀不起來是吧。