共计 8902 个字符,预计需要花费 23 分钟才能阅读完成。
背景
咱们有一家 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 = "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 中游荡,并且给你精准找进去。
探索 gen0 上的 student1 和 studnet2
先启动程序,抓一下 dump 文件。
0:000> !clrstack -l
ConsoleApp4.Program.Main(System.String[]) [C:dreamCsharpConsoleApp1ConsoleApp4Program.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:dreamCsharpConsoleApp1ConsoleApp4Program.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:dreamCsharpConsoleApp1ConsoleApp4Program.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 尽量少用,省的把外部的调配和回收算法搞乱了,非要用的话也要了解之后再依据本人的场景应用哈。
本篇就说到这里,心愿对你有帮忙