一:背景

1. 讲故事

我们有一家top级的淘品牌店铺,为了后续的加速计算,在程序启动的时候灌入她家的核心数据到内存中,灌入完成后内存高达100G,虽然云上的机器内存有256G,然被这么划掉一半看着还是有一点心疼的,可怜那些被挤压的小啰啰程序,本以为是那些List,HashSet,Dictionary需要动态扩容虚占了很多内存,也就没当一回事,后来过了一天发现内存回到了大概70多G,卧槽,不是所谓的集合虚占,而是GC没给我回收呀。。。

2. 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 objects的信息:

空闲块个数:168 + 368 + 11857034 = 11857570个

空闲块空间:1120008 + 8096 + 1052310524 = 1053438628 byte => 0.98G

惊讶吧~, 3.89G的堆,等待被释放的空间就有0.98G,占比高达25%,再看看第2代中有高达1185万个空闲块,说明堆碎片有多么严重。。。

所以等GC自己启动回收压缩释放不知道猴年马月,为了高效利用内存,不得已自己先给程序点个火,手工调用一下GC.Collect,让程序内存降到了 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尽量少用,省的把内部的分配和回收算法搞乱了,非要用的话也要理解之后再根据自己的场景使用哈。

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


如您有更多问题与我互动,扫描下方进来吧~


内存迟迟下不去,可能你就差一个GC.Collect的更多相关文章

  1. 为了去重复,写了一个通用的比较容器类,可以用在需要比较的地方,且支持Lamda表达式

    为了去重复,写了一个通用的比较容器类,可以用在需要比较的地方,且支持Lamda表达式,代码如下: public class DataComparer<T>:IEqualityCompare ...

  2. linux面试题:删除一个目录下的所有文件,但保留一个指定文件

    面试题:删除一个目录下的所有文件,但保留一个指定文件 解答: 假设这个目录是/xx/,里面有file1,file2,file3..file10 十个文件 [root@oldboy xx]# touch ...

  3. Java基础面试操作题: File IO 文件过滤器FileFilter 练习 把一个文件夹下的.java文件复制到另一个文件夹下的.txt文件

    package com.swift; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File ...

  4. node遍历给定目录下特定文件,内容合并到一个文件

    遍历目录用了fs.readdir这个异步方法,得到当前目录下所有的文件和目录的一个数组.然后判断: if文件,并且后缀符合设定的规则(本文例子是符合后缀ts,js)直接用同步方法写入, if目录,继续 ...

  5. Linux:删除一个目录下的所有文件,但保留一个指定文件

    面试题:删除一个目录下的所有文件,但保留一个指定文件 解答: 假设这个目录是/xx/,里面有file1,file2,file3..file10  十个文件 [root@oldboy xx]# touc ...

  6. PyCharm下创建并运行我们的第一个Django项目

    PyCharm下创建并运行我们的第一个Django项目 准备工作: 假设读者已经安装好python 2x或3x,以及安装好Django,以及Pycharm 1. 创建一个新的工程 第一次运行Pycha ...

  7. Linux将一个文件夹或文件夹下的所有内容复制到另一个文件夹

    Linux将一个文件夹或文件夹下的所有内容复制到另一个文件夹     1.将一个文件夹下的所有内容复制到另一个文件夹下 cp -r /home/packageA/* /home/cp/packageB ...

  8. 解决UDT中内存下不去的问题

         使用UDT库,编写简单的网络通信程序,发现了一个问题,关闭一部分连接后,程序占用内存并没有变化.      比如先连接500个,再连接另500个,先关掉后面500个,程序占用内存降一半,再关 ...

  9. <转>Linux环境进程间通信(五): 共享内存(下)

    http://www.ibm.com/developerworks/cn/linux/l-ipc/part5/index2.html 系统调用mmap()通过映射一个普通文件实现共享内存.系统V则是通 ...

随机推荐

  1. MyBatis通用 Mapper4使用小结

    官网地址: http://www.mybatis.tk/ https://gitee.com/free 1.使用springboot,添加依赖: 使用tk的mybatis后不需要引用官方原生的myba ...

  2. CopyOnWriteArrayList线程安全的集合

    CopyOnWriteArrayList是线程安全的集合.本身就是安全的,同时只能被一个进程所访问. 属于JUC并发编程里面的内容. public static void main(String[] ...

  3. [算法]Miller-Robbin素数判定

    目录 一.实现原理 二.应用 判断一个正整数是否为素数 三.小结 一.实现原理 我们以前都是怎么判断素数的呢: 试除法: 若一个正整数N为合数,则存在一个能整除N的数k,其中\(2\leqslant ...

  4. java nio消息半包、粘包解决方案

    问题背景 NIO是面向缓冲区进行通信的,不是面向流的.我们都知道,既然是缓冲区,那它一定存在一个固定大小.这样一来通常会遇到两个问题: 消息粘包:当缓冲区足够大,由于网络不稳定种种原因,可能会有多条消 ...

  5. d3.js v4曲线图的拖拽功能实现Zoom

    zoom缩放案例 源码:https://github.com/HK-Kevin/d...:demo:https://hk-kevin.github.io/d3...: 原理:通过zoom事件来重新绘制 ...

  6. 微服务统计,分析,图表,监控, 分布式追踪一体化的 HttpReports 在 .Net Core 的应用

    前言介绍 HttpReports 是针对.Net Core 开发的轻量级APM系统,基于MIT开源协议, 使用HttpReports可以快速搭建.Net Core环境下统计,分析,图表,监控,分布式追 ...

  7. layui.laytpl 模板引擎用法

    目录 layui下载地址: 最终效果: 模板引擎文档 手册地址: 以下是代码思路: layui下载地址: https://www.layui.com/ 最终效果: 模板引擎文档 手册地址: https ...

  8. powershell提示无法将“”项识别

    解决: 完成! 解释: 权限问题.Powershell脚本的4种执行权限介绍,Windows默认不允许任何脚本运行,我们可以使用"Set-ExecutionPolicy"cmdle ...

  9. [护网杯2018] easy_laravel

    前言 题目环境 buuoj 上的复现,和原版的题目不是完全一样.原题使用的是 nginx + mysql 而 buuoj 上的是 apache + sqlite composer 这是在 PHP5.3 ...

  10. 进阶 Linux基本命令-1

    vmware三种网络模式1,桥接虚拟机直接连接外网,局域网.宿主机电脑不提供路由. 2,NAT网络地址转换,家庭网 3,host only 只能和宿主电脑打交道 Linux命令形式 命令 +[参数]+ ...