Advertisement

内存迟迟下不去,可能你就差一个GC.Collect

阅读量:

背景

我们拥有一家位于行业顶端的淘宝品牌店铺,在推进加速计算的过程中,在程序启动时将该店铺的核心数据加载至内存中。此时内存容量达到100GB(即1TB),而云端服务器的内存配置为256GB(即半TB)。然而发现剩余内存空间略显不足(即未达到预期),导致资源占用异常高(即那些List、HashSet、Dictionary等动态容器需要预留额外内存空间)。本以为是那些List、HashSet、Dictionary等动态容器需要动态扩容预留大量内存空间(即预期会预留较多额外空间),也就未予过多关注。然而经过一段时间运行后发现剩余内存空间约为70%左右(即系统并未像预期那样释放这些占用的空间),然而发现系统并未像预期那样释放这些占用的空间(原来GC没有回收这些占用的空间)。

windbg验证一下

为了检验我的主张,在测试环境中进行数据采集任务,并于当晚完成所有数据处理工作。

!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 (4,695,636,656 bytes) 然后采用以下方法将上述字节数转换为GB单位: 计算方法是 4,695,636,656 ÷ 1,073,741,824 =约 4.39 GB

然后我们再评估一下第三代系统中还有多少未被占用的对象可供使用以及它们所占的空间有多大。为了便于查阅这些信息,请大家借助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.89GB的运行时空间堆中等待回收并释放出可用空间的区域共有约0.98GB的空间块,并占据了总运行时空间的25%比例;而查看第二轮内存管理情况时发现,在第二轮中存在数量庞大的1185万个对象待回收。这表明,在整个加载过程中至少触发了两次垃圾回收进程以处理这些内存碎片资源。

由于GC(垃圾回收)机制未自动启动,导致用户无法预知回收时间。通过手动激活内存管理机制,并计算出 3.89 - 0.98 = 2.91 G 的可用内存空间。

对GC代机制的理解

许多开发者在学习垃圾回收器中的代管理机制时可能会感到困惑。
有时即使读过相关书籍,在理论层面的理解也可能较为浅显。
无法亲自验证书中提到的内容。
我也未能完全掌握这一技术细节。
为了能够深入探讨这一技术而不放弃努力。
希望以上改写能让您有所收获!如果有其他问题,请随时告诉我哦~ 😊

CLR堆模型

当CLR不小心错入程序世界时,在内存中为你设置了两个专门的区域——一个是'小对象池'(Small Object Pool),另一个是'大对象池'(Large Object Pool)。默认情况下,默认容量设定为83,000字节作为两者的分水岭。当然你可以根据具体需求进行个性化设置。每个池子中的可用空间是由多个内存块拼接而成的。可能会让你感到困惑。如果还有疑问或者需要进一步了解,请参考附带的图表说明

image.png

对临时内存段的解释

看完上图,可能大家有两个疑问:

为啥小对象堆中有一个临时内存段?

由于_CLR做出了许多假设,在处理特定阶段(如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个内存段其中gen₀的起始地址为₀₀₀₀₀₁₉_b₀fc₆₆b₄8gen₁的起始地址则位于₀₀₀₀₀₁₉_b₀f7₃b₁₃8这两个地址都落在第十五个内存块₀₀₀₀_₁9_b_⁰e₂⁰⁰⁰⁰到₂¹⁷₈$(即十进制的2539114₁6)内剩余的所有内存块均被gen₂占用建议大家先仔细查看几遍然后稍后再观看我的演示过程

临时内存段大小是多少?

这一段的空间占用情况取决于使用的计算机架构以及GC运行在工作站环境还是服务器环境中。然而通过参考微软文档(MSDN),我们可以获得一些关键信息以帮助做出选择。为了方便大家理解这些配置细节,在线资源中提供了相关图表以便参考。

image.png

我的本机是x64版本,工作站模式,可以通过 !eeversion 查看一下。

复制代码
 0:000> !eeversion

    
 4.8.3801.0 free
    
 Workstation mode
    
 SOS Version: 4.8.3801.0 retail build
    
    
    
    
    代码解释

在图中,其临时内存块的最大容量设定为256MB。建议再次使用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中的生成机制?因为每个GC周期内三个generator运行时空间会根据GC操作而不断变化的特性,在特定时间段内难以确定每个generator所对应的内存分配阈值。

下面让我们来详细解释第三代内存回收机制。当gen0内存区满时会触发垃圾回收(GC)机制,将当前存活的对象复制到下一个空闲的gen1内存区中,并将已死亡的对象释放回空闲空间。在特定条件下,如果gen1内存区再次变得饱和,则会迫使系统从父进程处请求新的可用内存空间。通过这种机制,在实际运行中一个程序可能占用多达14个不同的内存区域。

代机制原理的代码演示

我也提到了一个观点,在线课程中许多人对这个理论有所了解:不清楚如何验证这一理论?在这里我可以为大家展示一个简单的验证方法:首先我会介绍相关的基础知识背景,并通过实际案例来说明其应用价值。

复制代码
     public static void Main(string[] args)

    
     {
    
         Student student1 = new Student() { UserName = "cnblogs", Email = "cnblogs@qq.com" };
    
         Student student2 = new Student() { UserName = "", Email = "@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文件。

image.png
复制代码
 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 和 student2 的对应地址分别为 ......;同时发现 gen 位置起始地址位于 ... 正好位于 gen 区域内;为了便于理解,在此附图说明

image.png

探究 student1 被消灭,student2进入gen1

按回车键后,在运行后续代码的过程中会将student1字段被设为null值,并立即触发垃圾回收操作以释放不必要的内存资源;随后立即查看堆中的状态有何变化以获取进一步的操作指示

复制代码
 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.
    
    
    
    
    代码解释

如果理解了前一案例的内容,看来这个部分相对简单。能够清晰地观察到student2位于gen1区间段,值得注意的是,在起始地址位置,可以看到,在gen1区域的空间有所扩展。为了进一步分析结果的趋势,我继续画一张图。

image.png

探究student2 送上了 gen2

image.png
复制代码
 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。这样能节省资源并避免浪费。不过如果必须使用它的话,请确保充分理解其工作原理,并根据具体的项目需求进行适当配置以适应不同的应用场景。

本篇就说到这里,希望对你有帮助

全部评论 (0)

还没有任何评论哟~