溫故知新:老鐵,WeakReference了解一下?


本文供稿——大師兄

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

什么是弱引用

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

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的異常,然后正在吃雞的你突然接到老板的電話:“明天你可以不用來了,又出線上事故!”。

201.png

發生該慘案的原因是,大量的對象實例占用了大量的內存。

此時你可能會問:.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和內存使用情況:

202.png

無請求狀態下應用已分配(托管對象占用的內存量)內存接近10M,工作集(進程的虛擬地址空間中當前駐留在物理內存中的頁集)內存接近80M。

  • 強引用測試

    我們模擬一個用戶請求5000次的場景

    203.png

    待程序run一會兒,得到下面的一個折線圖:

    204.png

    可以看出,隨着時間的推移,我們的內存呈現出了直線增長的狀態。

  • 弱應用測試

    為了測試公平起見,先停止應用程序然后再重新啟動,模擬請求參數和上述保持一致:

    205.png

    同樣等程序run一會兒,得到下面的一個折線圖:

    206.png

相信看到這兒,您應該已經可以看出差距來了。

在強引用的折線圖中,雖然間隔一段時間 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方法,應用的是短弱引用功能。

總結

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

  • 當對象占用大量內存,但通過垃圾回收功能回收以后很容易重新創建的對象特別適合使用弱引用。

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

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

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

  • 避免將弱引用作為內存管理問題的自動解決方案, 而應開發一個有效的緩存策略來處理應用程序的對象。

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

我們的微信公眾號二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM