本文供稿——大師兄
弱引用是個什麼鬼?大白話說就是不那麼強的引用(哈哈,純屬玩笑,實際可不是這樣滴),那強引用又是個什麼鬼?他們有什麼用處?問題有點迷,君閱完這篇文章後或許你心中就有答案了……
什麼是弱引用
在解釋弱引用之前,我們可以先來看看什麼是強引用
。以下是來自官方的定義:
The garbage collector cannot collect an object in use by an application while the application's code can reach that object. The application is said to have a strong reference to the object.
翻譯成大白話就是:應用程式的程式碼可以訪問一個正由該程式使用的物件,垃圾回收器就不能回收該物件,就可以認為應用程式對該物件具有強引用。
我們平常用的都是物件的強引用。這種情況下,假如該物件的例項還在被其他地方所使用,那麼 GC
是不能回收當前物件的。
如果在實際開發中,你建立了一個很大的物件而且該物件還會不斷的“生長”(比如不斷往一個靜態List中新增大文字的string,而該List忘記在操作之後被清空)。
到最後你或許會得到一個OutOfMemoryException
的異常,然後正在吃雞的你突然接到老闆的電話:“明天你可以不用來了,又出線上事故!”。
發生該慘案的原因是,大量的物件例項佔用了大量的記憶體。
此時你可能會問:.NET不是自帶了 GC
(垃圾回收)嗎? 他不是會在某些時刻把這些物件給釋放掉嗎?
然而,就像上文所說,因為 GC
認定該物件正在被使用,所以就“不敢”釋放該部分資源。
這也提醒了我們,在使用靜態資源或者單例物件的時候(特別是靜態List、Dictionary等集合)。要特別注意資源釋放的問題。
那麼有木有一個“神器”既可以保持物件的引用,在適當時候 GC
又 “敢” 回收掉這個物件呢?
此時就可以介紹我們本篇的主角了:弱引用
。
“ 弱引用允許應用程式訪問物件,同時也允許垃圾回收器收集、回收相應的物件。”
在.NET
中,微軟給我們提供了WeakReference
,來解決上述問題:
WeakReference
我們以一個API服務以及結合本地快取例子來玩一玩 WeakReference
,看看他們會發生什麼事情。詳細程式碼如下:
[Route("api/cache")]
[ApiController]
public class CacheController : ControllerBase
{
public CacheController()
{
Interlocked.Increment(ref DiagnosticsController.Requests);
}
// 不使用弱引用
private static Dictionary<long, User> UserCacheStrongReference = new();
// 使用弱引用
private static Dictionary<long, WeakReference<User>> UserCacheWeakReference = new();
[HttpGet]
[Route("StrongReference")]
public ActionResult<int> GetUserWithStrongReference()
{
User user = new User
{
Id = DateTime.Now.Ticks,
Name = new String('dsx', 10 * 1024),
Age = new Random().Next(),
Birthday = DateTime.Now
};
UserCacheStrongReference.TryAdd(user.Id, user);
return UserCacheStrongReference.Count;
}
[HttpGet]
[Route("WeakReference")]
public ActionResult<int> GetUserWithWeakReference()
{
User user = new User
{
Id = DateTime.Now.Ticks,
Name = new String('dsx', 10 * 1024),
Age = new Random().Next(),
Birthday = DateTime.Now
};
UserCacheWeakReference.TryAdd(user.Id, new WeakReference<User>(user));
return UserCacheWeakReference.Count;
}
}
public class User
{
public long Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public DateTime Birthday { get; set; }
}
溫馨提示:例子使用了Github上提供的一個示例應用(傳送門),它可以幫助我們用折線圖的方式呈現實時記憶體和GC資料。
上面的演示程式碼比較簡單,是我們日常開發中使用本地快取的常用方式,因此不在贅述。
當程式執行起來的時候,我們使用postjson工具
做壓力測試,以模擬大量使用者請求上述介面的場景。
先看看我們沒有任何請求的狀態下GC和記憶體使用情況:
無請求狀態下應用已分配(託管物件佔用的記憶體量)記憶體接近10M,工作集(程序的虛擬地址空間中當前駐留在實體記憶體中的頁集)記憶體接近80M。
強引用測試
我們模擬一個使用者請求5000次的場景
待程式run一會兒,得到下面的一個折線圖:
可以看出,隨著時間的推移,我們的記憶體呈現出了直線增長的狀態。
弱應用測試
為了測試公平起見,先停止應用程式然後再重新啟動,模擬請求引數和上述保持一致:
同樣等程式run一會兒,得到下面的一個折線圖:
相信看到這兒,您應該已經可以看出差距來了。
在強引用的折線圖中,雖然間隔一段時間 GC
就會進行一次回收(圖中中間位置的三角形)。,但是記憶體還是不斷的在增長。
這也符合我們的預期,應該 GC
不敢對 User
例項進行回收,從而導致字典中的資料越來越多。
而弱引用卻恰恰相反,每當 GC
進行一次回收,記憶體就像過山車一樣往下掉。最終一直穩定在 20MB。
還有就是強引用的 GC
進行垃圾回收的頻率比弱引用高很多。這是因為記憶體在往上增長時,GC
則會越頻繁的工作,因為他想趕快幫我們釋放資源,可哪知我們卻不斷建立大物件,“深深的傷害了它”。
從WeakReference中獲取引用的物件
獲取當前WeakReference
物件引用的物件很簡單,WeakReference
提供了一個Target
屬性以及TryGetTarget
方法。
我們通過這個屬性就可以獲取應用的物件,以上述示例為基礎,我們從快取中獲取User物件:
public User GetUserFromWeakReferenceDic(long userId)
{
if (UserCacheWeakReference.TryGetValue(userId, out WeakReference<User> user))
{
if (user.TryGetTarget(out User userInfo))
{
return userInfo;
}else
{
//當例項沒有的時候,證明它已經被GC所釋放了,我們往往需要再次建立它
}
}else
{
// .....
}
}
短弱引用和長弱引用
WeakReference
的另外一個建構函式 WeakReference(Object, Boolean)
,需要我們傳入一個bool型別的值,該值表示何時停止跟蹤物件:
長弱引用: 在物件的Finalize方法被執行以後,長弱引用將獲得保留,不過物件的某些成員變數或許已被回收,因此這種模式下需要謹慎使用這些變數。
短弱引用: 垃圾回收功能回收物件後,短弱引用的目標值會變為 null
,因此物件只在目標被回收前有效。 弱引用本身是託管物件,與其他任何託管物件一樣需要經過垃圾回收,需要注意的是,如果物件型別不包含Finalize方法,應用的是短弱引用功能。
總結
弱引用不是“銀彈”,那麼我們應該在什麼時使用弱引用呢?
- 當物件佔用大量記憶體,但通過垃圾回收功能回收以後
很容易重新建立的物件
特別適合使用弱引用。
因此在使用弱引用時我們需要關注以下準則:
僅在必要時使用長弱引用,因為在終結後物件的狀態不可預知。
避免對小物件使用弱引用,因為指標本身可能和物件一樣大,或者比物件還大。
避免將弱引用作為記憶體管理問題的自動解決方案, 而應開發一個有效的快取策略來處理應用程式的物件。
最後的最後,希望大家 點贊,關注,一鍵三連 走一波。