本文供稿——大師兄

弱引用是個什麼鬼?大白話說就是不那麼強的引用(哈哈,純屬玩笑,實際可不是這樣滴),那強引用又是個什麼鬼?他們有什麼用處?問題有點迷,君閱完這篇文章後或許你心中就有答案了……

什麼是弱引用

在解釋弱引用之前,我們可以先來看看什麼是強引用。以下是來自官方的定義:

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方法,應用的是短弱引用功能。

總結

弱引用不是“銀彈”,那麼我們應該在什麼時使用弱引用呢?

  • 當物件佔用大量記憶體,但通過垃圾回收功能回收以後很容易重新建立的物件特別適合使用弱引用。

因此在使用弱引用時我們需要關注以下準則:

  • 僅在必要時使用長弱引用,因為在終結後物件的狀態不可預知。

  • 避免對小物件使用弱引用,因為指標本身可能和物件一樣大,或者比物件還大。

  • 避免將弱引用作為記憶體管理問題的自動解決方案, 而應開發一個有效的快取策略來處理應用程式的物件。

最後的最後,希望大家 點贊,關注,一鍵三連 走一波。