PerfView專題 (第十六篇): 如何洞察C#托管堆記憶體的 "黑洞現象"

来源:https://www.cnblogs.com/huangxincheng/archive/2023/07/24/17576542.html
-Advertisement-
Play Games

## 一:背景 ### 1. 講故事 首先聲明的是這個 `黑洞` 是我定義的術語,它是用來表示 `記憶體吞噬` 的一種現象,何為 `記憶體吞噬`,我們來看一張圖。 ![](https://img2023.cnblogs.com/blog/214741/202307/214741-202307241003 ...


一:背景

1. 講故事

首先聲明的是這個 黑洞 是我定義的術語,它是用來表示 記憶體吞噬 的一種現象,何為 記憶體吞噬,我們來看一張圖。

從上面的 卦象圖 來看,GCHeap 的 Allocated=852MCommitted=16.6G,它們的差值就是 分配緩衝區=16G,緩衝區的好處就是用空間換時間,弊端就是會實實在在的侵占記憶體,擠壓其他程式的生存空間。

二:黑洞現象

1. 為什麼會有黑洞現象

萬事皆有因果,今生的是前世種的,換句話說是程式曾經有大量及頻繁的創建臨時對象,讓GC不自主的痙攣,小攣傷神,大攣傷身,所以GC為了避免大攣的發生,就大量的囤積本應該釋放掉的記憶體,目的就是防止未來某個時刻再次有大記憶體分配的發生。

2. 重現今生的果

我相信因果關係大家都弄清楚了,但口說無憑,還得用代碼證明一下不是?為了模擬GC痙攣,上一段測試代碼。


    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddAuthorization();
            var app = builder.Build();

            // Configure the HTTP request pipeline.
            app.UseAuthorization();

            app.MapGet("/mytest", (HttpContext httpContext) =>
            {
                return MyTest();
            });

            app.MapGet("/gc", (HttpContext httpContext) =>
            {
                GC.Collect();

                return 1;
            });

            app.Run();
        }

        public static string MyTest()
        {
            List<string> list = new List<string>();

            for (int i = 0; i < 100000000; i++)
            {
                list.Add(i.ToString());
            }

            return "ok";
        }
    }

代碼非常簡單,每請求一次 /mytest 都會分配一個 1億 大小 List<string> 數組,而這個 List<string> 又是一個臨時對象,後續會被 GC 回收,接下來我們多請求幾次來調戲一下 GC,看他如何痙攣,截圖如下:

從卦中看,我當前請求了 6 次,記憶體峰值達到了 12G,因為是臨時對象,稍稍有一點回落,但此時已經撐成一個大胖子了,接下來我們用 WinDbg 附加一下,觀察下 Allocated 和 Committed 閾值。


0:033> !eeheap -gc

========================================
Number of GC Heaps: 12
----------------------------------------
...
Heap 11 (0000023513f26c10)
generation 0 starts at 23351c3aab8
generation 1 starts at 233484c38e0
generation 2 starts at 233484c1000
ephemeral segment allocation context: none
Small object heap
         segment            begin        allocated        committed allocated size          committed size         
    0233484c0000     0233484c1000     02335c794ad0     023379ad2000 0x142d3ad0 (338508496)  0x31612000 (828448768) 
Large object heap starts at 234384c1000
         segment            begin        allocated        committed allocated size          committed size         
    0234384c0000     0234384c1000     0234384c1018     0234384e2000 0x18 (24)               0x22000 (139264)       
Pinned object heap starts at 234f84c1000
         segment            begin        allocated        committed allocated size          committed size         
    0234f84c0000     0234f84c1000     0234f84c1018     0234f84c2000 0x18 (24)               0x2000 (8192)          
------------------------------
GC Allocated Heap Size:    Size: 0x14f241378 (5622731640) bytes.
GC Committed Heap Size:    Size: 0x2b125c000 (11561975808) bytes.

從卦中看當前已經有 6G 的緩衝區了,為了讓緩衝區更誇張,我們故意手工觸發一次 GC 即請求 /gc,觸發了GC之後,記憶體從 10G 回落到了 7G 就不再降了,截圖如下:

從卦中看,這兩個指標就更誇張了,GC 堆只有 1.1M 的對象,但預留了 7.1G 的記憶體。

這個GC表現不管在 道德 還是 倫理 上都說不通的。

3. 找到前世的因

要想找到前世的因,手段有很多,比如用 WinDbg 觀察前世的托管堆,從殘留的 Committed - Allocated上就能找到因,也可以使用 PerfView 實時觀察,這裡我們採用後者來洞察,使用預設的 Command 參數。


PerfView.exe  "/DataFile:PerfViewData.etl" /BufferSizeMB:256 /StackCompression /CircularMB:500 /ClrEvents:GC,Binder,Security,AppDomainResourceManagement,Contention,Exception,Threading,JITSymbols,Type,GCHeapSurvivalAndMovement,GCHeapAndTypeNames,Stack,ThreadTransfer,Codesymbols,Compilation /NoGui /NoNGenRundown /Merge:True /Zip:True collect

採集一段時間後停止採集,接下來雙擊 GC Heap Net Mem (Coarse Sampling) Stacks 選項再選擇 WebApplication1 進程,通過 MaxMetric 指標看到曾經峰值達到了 10.9G,截圖如下:

毫無疑問的說,記憶體峰值的時候必有妖怪,可以將峰值填入到 End 文本框中,然後雙擊記憶體占比最高的 System.String[],觀察下它是誰分配的,截圖如下:

從截圖中可以清晰的看到,原來是 Program.MyTest() 造的孽,至此真相大白。

4. 尋求化解之道

化解之道有很多:

  • 修改 GC 模式

簡而言之就是將 Server GC 改成 Workstation GC ,參考代碼如下:


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <ServerGarbageCollection>false</ServerGarbageCollection>
  </PropertyGroup>

</Project>

  • 修改 Heap 個數

預設情況一個 cpucore 有一個 heap,我們可以儘量的減少 heap.count 的個數,比如將 12 個改成 2 個。參考代碼如下:


{
   "runtimeOptions": {
      "configProperties": {
         "System.GC.HeapCount": 2
      }
   }
}

  • 大事化小

導致今世的 是因為在記憶體中短時的出現大對象,可以將大對象拆分成多批次的小對象處理,這樣可以達到後浪推前浪的的記憶體復用,從源頭上繞過這個問題。

三:總結

記憶體黑洞 雖不算 CLR 的一個bug,但絕對是 CLR 可優化的一個空間,分析這類問題是需要經驗性的,分享出來供後來者少踩坑吧,畢竟在我的分析旅程中至少遇到了3次

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡介: 在現代軟體開發中,全球唯一標識符(UUID)在許多場景中發揮著重要的作用。UUID是一種128位的唯一標識符,它能夠保證在全球範圍內不重覆。在Go語言中,我們可以使用第三方庫`github.com/google/uuid`來方便地生成UUID。本文將介紹如何使用這個庫來生成不同版本的UUID ...
  • 睡不著閑逛,在GitHub上看到一個挺實用的開源項目:**Spring Startup Analyzer**。 從項目名稱中就大概能猜到,這是一個分析Spring應用啟動過程的工具。Spring Startup Analyzer通過採集Spring應用啟動過程的數據,進而生成一個互動式的分析報告,幫 ...
  • # 環境要求 - `Windows XP` 及以上。 - `Windows 10` 、 `Windows 11` 在 `Windows 功能` 中勾選 `.NET Framework 3.5 (包括 .NET 2.0 和 3.0)` 。 # 前置知識 ```VBScript WSH.Echo Emp ...
  • 將本地的改動極速同步到遠程服務端,並自動生效,掌握此技能,開發調試會更高效 ### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_ ...
  • 經過一段時間的準備,新的一期【ASP.NET Core MVC開發實戰之商城系統】已經開始,在之前的文章中,講解了商城系統的整體功能設計,頁面佈局設計,環境搭建,系統配置,及首頁商品類型,banner條,友情鏈接等功能的開發,今天繼續講解首頁的降價促銷,新品爆款等內容,僅供學習分享使用,如有不足之處... ...
  • # Unity UGUI的RawImage(原始圖片)組件的介紹及使用 ## 1. 什麼是RawImage組件? RawImage是Unity UGUI中的一個組件,用於顯示原始圖片。與Image組件不同,RawImage可以直接顯示原始圖片的像素數據,而不需要經過額外的處理。 ## 2. RawI ...
  • 在使用`Winform`開發桌面應用時,工具箱預先提供了豐富的基礎控制項,利用這些基礎控制項可以開展各類項目的開發。但是或多或少都會出現既有控制項無法滿足功能需求的情況,或者在開發類似項目時,我們希望將具有相同功能的模板封裝成一個標準控制項等,在這些場景下,`winform`自帶的控制項就有些乏力了,需要我們 ...
  • ## 引言 **dynamic** 是 `Framework 4.0` 就出現特性,它的出現讓 C# 具有了弱語言類型的特性。編譯器在編譯的時候不再對類型進行檢查,預設 **dynamic** 對象支持開發者想要的任何特性。 ## dynamic 介紹 在C#中,dynamic是一種類型,它允許你在 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...