內存遲遲下不去,可能你就差一個GC.Collect

一:背景

1. 講故事

咱們有一家top級的淘品牌店鋪,爲了後續的加速計算,在程序啓動的時候灌入她家的核心數據到內存中,灌入完成後內存高達100G,雖然雲上的機器內存有256G,然被這麼劃掉一半看着仍是有一點心疼的,可憐那些被擠壓的小囉囉程序😄😄😄,本覺得是那些List,HashSet,Dictionary須要動態擴容虛佔了不少內存,也就沒當一回事,後來過了一天發現內存回到了大概70多G,臥槽,不是所謂的集合虛佔,而是GC沒給我回收呀。。。程序員

2. windbg驗證一下

爲了驗證個人說法,我就不去生產抓這個龐然大物的dump了,去測試環境給你們抓一個,晚上清蒸。算法

!eeheap -gc 查看gc信息express

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000019b0fc66b48
generation 1 starts at 0x0000019b0f73b138
generation 2 starts at 0x0000019a5da81000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000019a5da80000  0000019a5da81000  0000019a6da7ffb8  0xfffefb8(268431288)
0000019a00000000  0000019a00001000  0000019a0ffffe90  0xfffee90(268430992)
0000019a10000000  0000019a10001000  0000019a1ffffeb0  0xfffeeb0(268431024)
0000019a20000000  0000019a20001000  0000019a2fffffb0  0xfffefb0(268431280)
0000019a30000000  0000019a30001000  0000019a3ffffc50  0xfffec50(268430416)
0000019a40000000  0000019a40001000  0000019a4fffffc8  0xfffefc8(268431304)
0000019a7aad0000  0000019a7aad1000  0000019a8aacfd60  0xfffed60(268430688)
0000019a8cbf0000  0000019a8cbf1000  0000019a9cbefe10  0xfffee10(268430864)
0000019a9cbf0000  0000019a9cbf1000  0000019aacbefcb8  0xfffecb8(268430520)
0000019aacbf0000  0000019aacbf1000  0000019abcbefd18  0xfffed18(268430616)
0000019abcbf0000  0000019abcbf1000  0000019accbefd68  0xfffed68(268430696)
0000019accbf0000  0000019accbf1000  0000019adcbefcf8  0xfffecf8(268430584)
0000019adcbf0000  0000019adcbf1000  0000019aecbefdc0  0xfffedc0(268430784)
0000019af0e20000  0000019af0e21000  0000019b00e1ff28  0xfffef28(268431144)
0000019b00e20000  0000019b00e21000  0000019b10047178  0xf226178(253911416)
Large object heap starts at 0x0000019a6da81000
         segment             begin         allocated              size
0000019a6da80000  0000019a6da81000  0000019a756d0480  0x7c4f480(130348160)
0000019b10e20000  0000019b10e21000  0000019b133ca330  0x25a9330(39490352)
Total Size:              Size: 0xf940ee70 (4181782128) bytes.
------------------------------
GC Heap Size:            Size: 0xf940ee70 (4181782128) bytes.

從最後一行能夠看到堆大小: GC Heap Size: Size: 0xf940ee70 (4181782128) bytes. 而後將4181782128 byte 轉化爲GB: 4181782128/1024/1024/1024= 3.89G服務器

而後再來看一下3代中有多少須要free的對象,佔了多少空間,爲了方便查看,你們能夠用一下sosex擴展,提供了不少方便的方法。測試

!dumpgen xxxx 依次把0,1,2 三個代中的free空間統計出來。ui

0:000> !dumpgen 0 -free -stat
       Count      Total Size      Type
-------------------------------------------------
         168      1,120,008   **** FREE ****

168 objects, 1,120,008 bytes

0:000> !dumpgen 1 -free -stat
       Count      Total Size      Type
-------------------------------------------------
         368          8,096   **** FREE ****

368 objects, 8,096 bytes

0:000> !dumpgen 2 -free -stat
       Count      Total Size      Type
-------------------------------------------------
  11,857,034  1,052,310,524   **** FREE ****

11,857,034 objects, 1,052,310,524 bytes

從上面輸出能夠看到,三個代中須要free的信息:lua

對象有:168 + 368 + 11857034 = 11857570個操作系統

空間:1120008 + 8096 + 1052310524 = 1053438628 byte => 0.98G線程

驚訝吧~, 3.89G的堆,等待被釋放的空間有0.98G,佔比高達25%,再看看第2代中有高達1185萬的對象須要清理,說明在整個加載過程當中,GC至少被觸發2次。。。3d

因此等GC本身啓動回收不知道猴年馬月,爲了高效利用內存,不得已本身先給程序點個火,讓程序內存降到了 3.89 - 0.98 = 2.91 G

二:對GC代機制的理解

有很多程序員對gc中的代管理機制不是特別清楚,或者看過書以後理解也停留在理論上,無法去驗證書中所說,其實我也不是特別理解,😄😄😄,做爲一個準備好好玩自媒體人,不能讓您白來一趟哈。

1. CLR堆模型

當CLR不當心錯入程序世界的時候,會給你分配兩個堆,一個叫作小對象堆,一個叫作大對象堆,默認是以83k做爲大小堆的分界線,固然你也能夠自定義配置,堆上的空間由不少的內存段拼成的,可能你有點蒙,我畫張圖吧。

2. 對臨時內存段的解釋

看完上圖,可能你們有兩個疑問:

<1> 爲啥小對象堆中有一個臨時內存段?

這是由於CLR作了不少假設,它假設在gen0和gen1上回收的對象會特別多,因此沒事就上去轉轉,CLR爲了方便GC快速清理回收壓縮。。。就將gen0和gen1都放置在這個臨時內存段上。

你可能要問,有證據嗎??? 我就拿剛纔的4G程序說話吧。

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000019b0fc66b48
generation 1 starts at 0x0000019b0f73b138
generation 2 starts at 0x0000019a5da81000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000019a5da80000  0000019a5da81000  0000019a6da7ffb8  0xfffefb8(268431288)
0000019a00000000  0000019a00001000  0000019a0ffffe90  0xfffee90(268430992)
0000019a10000000  0000019a10001000  0000019a1ffffeb0  0xfffeeb0(268431024)
0000019a20000000  0000019a20001000  0000019a2fffffb0  0xfffefb0(268431280)
0000019a30000000  0000019a30001000  0000019a3ffffc50  0xfffec50(268430416)
0000019a40000000  0000019a40001000  0000019a4fffffc8  0xfffefc8(268431304)
0000019a7aad0000  0000019a7aad1000  0000019a8aacfd60  0xfffed60(268430688)
0000019a8cbf0000  0000019a8cbf1000  0000019a9cbefe10  0xfffee10(268430864)
0000019a9cbf0000  0000019a9cbf1000  0000019aacbefcb8  0xfffecb8(268430520)
0000019aacbf0000  0000019aacbf1000  0000019abcbefd18  0xfffed18(268430616)
0000019abcbf0000  0000019abcbf1000  0000019accbefd68  0xfffed68(268430696)
0000019accbf0000  0000019accbf1000  0000019adcbefcf8  0xfffecf8(268430584)
0000019adcbf0000  0000019adcbf1000  0000019aecbefdc0  0xfffedc0(268430784)
0000019af0e20000  0000019af0e21000  0000019b00e1ff28  0xfffef28(268431144)
0000019b00e20000  0000019b00e21000  0000019b10047178  0xf226178(253911416)
Large object heap starts at 0x0000019a6da81000
         segment             begin         allocated              size
0000019a6da80000  0000019a6da81000  0000019a756d0480  0x7c4f480(130348160)
0000019b10e20000  0000019b10e21000  0000019b133ca330  0x25a9330(39490352)
Total Size:              Size: 0xf940ee70 (4181782128) bytes.
------------------------------
GC Heap Size:            Size: 0xf940ee70 (4181782128) bytes.

從上面gc信息中能夠看到小對象堆中目前有 15個內存段, 大對象堆有2個內存段, gen0的起始地址爲0x0000019b0fc66b48,gen1的起始地址爲0x0000019b0f73b138, 都落在了第15個內存段內 0000019b00e20000 0000019b00e21000 0000019b10047178 0xf226178(253911416),其他內存段都被 gen2 佔領,若是你們有點亂,先多看幾遍,等一下看個人演示。

<2> 臨時內存段大小是多少?

這個段的大小,須要看是x64仍是x86機器,還要看GC是工做站模式仍是服務器模式,不過msdn幫咱們總結了,https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals , 截個圖給你們看一下。

個人本機是x64版本,工做站模式,能夠經過 !eeversion 查看一下。

0:000> !eeversion
4.8.3801.0 free
Workstation mode
SOS Version: 4.8.3801.0 retail build

對應圖中,個人臨時內存段的最大內存是256M,再回過頭用4G程序的來驗證一下內存段大小,用 allocated - begin 便可。

ephemeral segment allocation context: none
         segment             begin         allocated              size
0000019b00e20000  0000019b00e21000  0000019b10047178  0xf226178(253911416)

0:000> ? 0000019b10047178 - 0000019b00e21000
Evaluate expression: 253911416 = 00000000`0f226178

二者差值爲 253911416 byte => 242M ,能夠看出離256M不遠了,等到了256M又要觸發GC啦。。。。

3. 代機制簡介

有了上面的基礎,我以爲你對GC的gen機制應該明白了,因爲3個gen運行時預約空間是隨GC觸發隨時變更,因此就不知道某個時刻各個gen當時的空間觸發閾值。

接下來講一下三代的原理:當gen0滿了會觸發GC回收,將gen0中活對象送到gen1中,死的就消滅掉,當某時候gen1滿了,gen1的活對象會被送到gen2中,當下個某一次gen2滿了,就向操做系統申請新的內存段,因此你看到了4G程序佔用了多達14個內存段,就是這麼一個道理,沒什麼複雜的。

三:代機制原理的代碼演示

我剛纔也說了,不少人知道這個理論,不知道怎麼去驗證,這裏我就演示一下,先上代碼:

public static void Main(string[] args)
        {
            Student student1 = new Student() { UserName = "cnblogs", Email = "cnblogs@qq.com" };
            Student student2 = new Student() { UserName = "csdn", Email = "csdn@qq.com" };

            Console.WriteLine("兩個對象已建立!雙雙進入 Gen0");
            Console.Read();

            student1 = null;
            GC.Collect();

            Console.WriteLine("Student1 已從Gen0中抹掉,助力Student2上Gen1,是否繼續?");
            Console.ReadKey();

            GC.Collect();
            Console.WriteLine("再次助力Student2上Gen2");
            Console.ReadKey();

            Console.WriteLine("所有執行結束!");
            Console.ReadLine();
        }
    }

    public class Student
    {
        public string UserName { get; set; }
        public string Email { get; set; }
    }

代碼很簡單,就是想讓你看一下student1和student2如何在gen0,gen1,gen2中游蕩,而且給你精準找出來。

1. 探究 gen0 上的student1 和 studnet2

先啓動程序,抓一下dump文件。

0:000> !clrstack -l

ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 18]
    LOCALS:
        0x000000017d7feeb8 = 0x000001d0962c2f28
        0x000000017d7feeb0 = 0x000001d0962c2f48

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x000001d0962c1030
generation 1 starts at 0x000001d0962c1018
generation 2 starts at 0x000001d0962c1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
000001d0962c0000  000001d0962c1000  000001d0962c7fe8  0x6fe8(28648)
Large object heap starts at 0x000001d0a62c1000
         segment             begin         allocated              size
000001d0a62c0000  000001d0a62c1000  000001d0a62c9a68  0x8a68(35432)
Total Size:              Size: 0xfa50 (64080) bytes.
------------------------------
GC Heap Size:            Size: 0xfa50 (64080) bytes.

仔細看上面的輸出,從主線程的堆棧上能夠看到student1和studnet2的地址依次爲0x000001d0962c2f28, 0x000001d0962c2f48,而gen0的起始地址爲:0x000001d0962c1030,恰好落在 gen0 的區間內,可能你有點蒙,我畫一張圖。

2. 探究 student1 被消滅,student2進入gen1

按下Enter鍵,執行後續代碼將student1=null,再執行GC操做,看下堆中又是如何?

0:000> !clrstack -l
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 24]
    LOCALS:
        0x000000607e9fea50 = 0x0000000000000000
        0x000000607e9fea48 = 0x0000017f0dff2f38

000000607e9fec88 00007ff8e9396c93 [GCFrame: 000000607e9fec88] 
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000017f0dff6ea0
generation 1 starts at 0x0000017f0dff1018
generation 2 starts at 0x0000017f0dff1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
0000017f0dff0000  0000017f0dff1000  0000017f0dff8eb8  0x7eb8(32440)
Large object heap starts at 0x0000017f1dff1000
         segment             begin         allocated              size
0000017f1dff0000  0000017f1dff1000  0000017f1dff9a68  0x8a68(35432)
Total Size:              Size: 0x10920 (67872) bytes.
------------------------------
GC Heap Size:            Size: 0x10920 (67872) bytes.

若是弄明白了上一個案例,看這裏就很簡單了,很清楚的看到studnet2落在了gen1區間段,不過從起始地址上看,gen1的空間變大了。。。我繼續畫一張圖。

3. 探究student2 送上了 gen2

0:000> !clrstack -l
ConsoleApp4.Program.Main(System.String[]) [C:\dream\Csharp\ConsoleApp1\ConsoleApp4\Program.cs @ 28]
    LOCALS:
        0x000000d340bfebb0 = 0x0000000000000000
        0x000000d340bfeba8 = 0x00000217b5df2f38

000000d340bfede8 00007ff8e9396c93 [GCFrame: 000000d340bfede8] 
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000217b5df6f40
generation 1 starts at 0x00000217b5df6ea0
generation 2 starts at 0x00000217b5df1000
ephemeral segment allocation context: none
         segment             begin         allocated              size
00000217b5df0000  00000217b5df1000  00000217b5df8f58  0x7f58(32600)
Large object heap starts at 0x00000217c5df1000
         segment             begin         allocated              size
00000217c5df0000  00000217c5df1000  00000217c5df9a68  0x8a68(35432)
Total Size:              Size: 0x109c0 (68032) bytes.
------------------------------
GC Heap Size:            Size: 0x109c0 (68032) bytes.

很簡單,我就不畫圖了哈,student2的內存地址但是落在 gen2上哦~😄😄😄

四:總結

GC.Collect儘可能少用,省的把內部的分配和回收算法搞亂了,非要用的話也要理解以後再根據本身的場景使用哈。

本篇就說到這裏,但願對你有幫助


如您有更多問題與我互動,掃描下方進來吧~


相關文章
相關標籤/搜索