背景
我們有一家top級的淘品牌店鋪,為了後續的加速計算,在程式啟動的時候灌入她家的核心資料到記憶體中,灌入完成後記憶體高達100G,雖然雲上的機器記憶體有256G,然被這麼劃掉一半看着還是有一點心疼的,可憐那些被擠壓的小啰啰程式😄😄😄,本以為是那些List,HashSet,Dictionary需要動态擴容虛占了很多記憶體,也就沒當一回事,後來過了一天發現記憶體回到了大概70多G,卧槽,不是所謂的集合虛占,而是GC沒給我回收呀。
windbg驗證一下
為了驗證我的說法,我就不去生産抓這個龐然大物的dump了,去測試環境給大家抓一個,晚上清蒸。
!eeheap -gc 檢視gc資訊
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空間統計出來。
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的資訊:
對象有:168 + 368 + 11857034 = 11857570個,
空間:1120008 + 8096 + 1052310524 = 1053438628 byte => 0.98G。
驚訝吧~, 3.89G的堆,等待被釋放的空間有0.98G,占比高達25%,再看看第2代中有高達1185萬的對象需要清理,說明在整個加載過程中,GC至少被觸發2次。。。
是以等GC自己啟動回收不知道猴年馬月,為了高效利用記憶體,不得已自己先給程式點個火,讓程式記憶體降到了 3.89 - 0.98 = 2.91 G。
對GC代機制的了解
有不少程式員對gc中的代管理機制不是特别清楚,或者看過書之後了解也停留在理論上,沒法去驗證書中所說,其實我也不是特别了解,😄😄😄,作為一個準備好好玩自媒體人,不能讓您白來一趟哈。
CLR堆模型
當CLR不小心錯入程式世界的時候,會給你配置設定兩個堆,一個叫做小對象堆,一個叫做大對象堆,預設是以83k作為大小堆的分界線,當然你也可以自定義配置,堆上的空間由很多的記憶體段拼成的,可能你有點蒙,我畫張圖吧
對臨時記憶體段的解釋
看完上圖,可能大家有兩個疑問:
為啥小對象堆中有一個臨時記憶體段?
這是因為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 占領,如果大家有點亂,先多看幾遍,等一下看我的示範。
臨時記憶體段大小是多少?
這個段的大小,需要看是x64還是x86機器,還要看GC是工作站模式還是伺服器模式,不過msdn幫我們總結了,截個圖給大家看一下。
我的本機是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啦。。。。
代機制簡介
有了上面的基礎,我覺得你對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 = "[email protected]" };
Student student2 = new Student() { UserName = "csdn", Email = "[email protected]" };
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中遊蕩,并且給你精準找出來。
探究 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 的區間内,可能你有點蒙,我畫一張圖。
探究 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的空間變大了。。。我繼續畫一張圖。
探究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盡量少用,省的把内部的配置設定和回收算法搞亂了,非要用的話也要了解之後再根據自己的場景使用哈。
本篇就說到這裡,希望對你有幫助