内存迟迟下不去,可能你就差一个GC.Collect
0|1背景
我们有一家top级的淘品牌店铺,为了后续的加速计算,在程序启动的时候灌入她家的核心数据到内存中,灌入完成后内存高达100G,虽然云上的机器内存有256G,然被这么划掉一半看着还是有一点心疼的,可怜那些被挤压的小啰啰程序😄😄😄,本以为是那些List,HashSet,Dictionary需要动态扩容虚占了很多内存,也就没当一回事,后来过了一天发现内存回到了大概70多G,卧槽,不是所谓的集合虚占,而是GC没给我回收呀。
windbg验证一下
为了验证我的说法,我就不去生产抓这个庞然大物的dump了,去测试环境给大家抓一个,晚上清蒸。
!eeheap -gc 查看gc信息
从最后一行可以看到堆大小: 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空间统计出来。
从上面输出可以看到,三个代中需要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
。
0|1对GC代机制的理解
有不少程序员对gc中的代管理机制不是特别清楚,或者看过书之后理解也停留在理论上,没法去验证书中所说,其实我也不是特别理解,😄😄😄,作为一个准备好好玩自媒体人,不能让您白来一趟哈。
CLR堆模型
当CLR不小心错入程序世界的时候,会给你分配两个堆,一个叫做小对象堆,一个叫做大对象堆,默认是以83k作为大小堆的分界线,当然你也可以自定义配置,堆上的空间由很多的内存段拼成的,可能你有点蒙,我画张图吧。
对临时内存段的解释
看完上图,可能大家有两个疑问:
为啥小对象堆中有一个临时内存段?
这是因为CLR做了很多假设,它假设在gen0和gen1上回收的对象会特别多,所以没事就上去转转,CLR为了方便GC快速清理回收压缩。。。就将gen0和gen1都放置在这个临时内存段上。
你可能要问,有证据吗??? 我就拿刚才的4G程序说话吧。
从上面gc信息中可以看到小对象堆中目前有 15个内存段, 大对象堆有2个内存段, gen0的起始地址为0x0000019b0fc66b48,gen1
的起始地址为0x0000019b0f73b138
, 都落在了第15个内存段内 0000019b00e20000 0000019b00e21000 0000019b10047178 0xf226178(253911416)
,其余内存段都被 gen2 占领,如果大家有点乱,先多看几遍,等一下看我的演示。
临时内存段大小是多少?
这个段的大小,需要看是x64还是x86机器,还要看GC是工作站模式还是服务器模式,不过msdn帮我们总结了,截个图给大家看一下。
我的本机是x64版本,工作站模式,可以通过 !eeversion 查看一下。
对应图中,我的临时内存段的最大内存是256M,再回过头用4G程序的来验证一下内存段大小,用 allocated - begin 即可。
两者差值为 253911416 byte => 242M ,可以看出离256M不远了,等到了256M又要触发GC啦。。。。
代机制简介
有了上面的基础,我觉得你对GC的gen机制应该明白了,由于3个gen运行时预定空间是随GC触发随时变动,所以就不知道某个时刻各个gen当时的空间触发阈值。
接下来说一下三代的原理:当gen0满了会触发GC回收,将gen0中活对象送到gen1中,死的就消灭掉,当某时候gen1满了,gen1的活对象会被送到gen2中,当下个某一次gen2满了,就向操作系统申请新的内存段,所以你看到了4G程序占用了多达14个内存段,就是这么一个道理,没什么复杂的。
0|1代机制原理的代码演示
我刚才也说了,很多人知道这个理论,不知道怎么去验证,这里我就演示一下,先上代码:
代码很简单,就是想让你看一下student1和student2如何在gen0,gen1,gen2中游荡,并且给你精准找出来。
探究 gen0 上的student1 和 studnet2
先启动程序,抓一下dump文件。
仔细看上面的输出,从主线程的堆栈上可以看到student1和studnet2的地址依次为0x000001d0962c2f28, 0x000001d0962c2f48
,而gen0的起始地址为:0x000001d0962c1030
,刚好落在 gen0 的区间内,可能你有点蒙,我画一张图。
探究 student1 被消灭,student2进入gen1
按下Enter键,执行后续代码将student1=null,再执行GC操作,看下堆中又是如何?
如果弄明白了上一个案例,看这里就很简单了,很清楚的看到studnet2落在了gen1区间段,不过从起始地址上看,gen1的空间变大了。。。我继续画一张图。
探究student2 送上了 gen2
很简单,我就不画图了哈,student2的内存地址可是落在 gen2上哦~😄😄😄
0|1总结
GC.Collect尽量少用,省的把内部的分配和回收算法搞乱了,非要用的话也要理解之后再根据自己的场景使用哈。
本篇就说到这里,希望对你有帮助
__EOF__
作 者:码农架构
出 处:https://www.cnblogs.com/ibytecoding/p/13896485.html
关于博主:目前任职蚂蚁金服,「公众号:码农架构」专注于系统架构、高可用、高性能、高并发类技术分享!
版权声明:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本。
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!