这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」。
- 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!
- 📢本文作者:由webmote 原创,首发于 【掘金】
- 📢作者格言: 生活在于折腾,当你不折腾生活时,生活就开始折腾你,让我们一起加油!💪💪💪
前方高能预警,新手慎入!不听劝阻者,轻则郁闷堆积,重则生死看淡,对编程失去了念想,对生活失去了幻想!好了,心理强大到NB的可以忽略前方若干警示。为了探索.NET对象的内存分配和回收销毁,您可能需要准备一些调试的基本知识,比如上篇的<利用SOS扩展库进入高阶.NET6程序的调试>.以下例子来自.net 6技术支持。
我们的第一个对象,不是你初中暗恋的古灵精怪的小女孩,更不是你高中的神秘御姐范的初恋女友,她是地地道道的Object。
不信,我Show给你看。
1 | csharp复制代码public static int Main() |
掀开她神秘的盖头,她也只不是千千万万普通对象中的一员,非要说她有什么不同的话,那可能就是你想驯服她,并且你花费了你的宝贵时间,在她身上。
1 | csharp复制代码public class MaoniType |
美丽总是隐藏在朦胧之中,隔纱看美人,越看越迷人。
不过我们需要的不是肤浅的撩骚,让我们利用高级窥探工具,更加深入到灵魂的探索她。
当然,最最简单的探索工具,就是Windbg + SoS 扩展了。
至于工具的使用,不是重点,在这里就略过了,如果你还不会的话,那么就移步<利用SOS扩展库进入高阶.NET6程序的调试>瞧瞧,那里已经给你备好了下酒好菜。
闲话少叙,让我们直接打开工具,键入神秘指令,来个一指入魂吧。
1 | shell复制代码0:007> .load C:\Users\webmote.dotnet\sos\sos.dll |
没错,找到 ConsoleApp6.MaoniType
这个类名,这就是你心心念的 对象
No 1.
既然已经被你定位到了,那么就让我们继续深入吧, 现在只需要点她的牌牌就可以了。
1 | shell复制代码0:007> !DumpHeap /d -mt 00007ffc77c13798 |
现在,有了她第一手的资讯:
1 | shell复制代码姓名: Maoni/莫妮 |
让我们来看看GC地址空间的情况:
1 | shell复制代码0:007> !eeheap -gc |
你应该没忘记我们对象的地址吧?
莫妮的地址是 000002470000c0c8
,而新生段的分配信息我们也可以清晰的看到。
当然有关段
,估计仍然需要一大章节才能说明白吧,这里仅仅简单介绍下。
它是GC从操作系统采集内存的一个单位,实际内存申请和分配以及释放以
segment
(段)为单位;例如:
workstation GC
模式segment
大小为16M,server GC
模式segment
大小为64M。
Gen 0
和Gen 1
heap总是位于同一个段中,叫做ephemeral segment
(新生段),Gen 2
heap由0个或多个segments
组成,LOH
由1个或多个segments
组成.NET程序启动时CLR为heap创建2个
segment
,一个作为ephemeral segment
,另一个用于LOH
。
Full GC
后完全空闲的segments
将被释放掉,内存返回给操作系统
再次深入前,让我们来点小甜点,放松一下,看看四周的风景。
4.1 我们怎么用DRAM
不管怎么分配,我们都需要涉及到物理内存。
当然,我们并不支持使用物理内存!
我们使用虚拟内存(VM),这块有操作系统的哦VMM(虚拟内存管理器)提供。
操作系统引入了虚拟内存概念,使得我们能够:
- 每个进程都认为它有自己的内存空间,就好象国家的廉租房制度一样,让每个人都体验到家的温馨。
- 你可以请求更多的内存,甚至超过了物理内存大小,而管理器只会占用真正使用的物理内存;
- 重要的是,不需要VM分配为连续的了,实现了即抛即用。
VM的实现也很有意思,由操作系统提供页的支持:
- 由MMU(内存管理单元)实现
- 内存被分割为页(一般是4K)
- 虚拟内存到物理内存由页映射使用页表进行管理
- 无法映射到物理内存,会导致页失败错误
- 操作系统控制页映射转换
有很多技术实现更快的转换,比如页表缓存、TLB(Translation Lookaside Buffer)技术等。
4.2 物理页是怎么组织的?
- 当计算机启动后,Windows操作系统把来自DRAM的物理页整理为一个列表;
- 当有进程需要物理页分配时,它转变为WS(Working Set)的一部分
- 当一个物理页从WS移除后,它通过软件页故障或硬页故障返回到列表
- 硬页故障是非常耗时的,因此我们需要避免它
- 为了避免硬页故障,我们不能增加大于物理内存的堆栈(可以观察物理内存负载信息)
4.3 GC怎么从VM采集内存
- 保留内存
由于需要分页的原因,因此我们可以请求稍后可能使用的范围地址,它被称作保留内存(VirtualAlloc 使用 MEM_RESERVE)。当然保留内存不能保存任何数据。
- 提交内存
当我们需要在页存储上存储数据时,我们告诉操作系统,这叫提交内存。(VirtualAlloc使用MEM_COMMIT),提交操作成功后,保证你不会得到OOM异常。
保留内存操作是非常快的,当然你仍然需要增加一次用户态<–> 内核态的操作;提交内存也是非常快的…. 当然,知道你真正的保存数据。 而恰恰这个时候,有可能引起分配页故障,导致OOM。
- 保存数据
一切都oK了,我们呢就可以轻松保存数据了。
让我们回到从前,一如第一次初见。
5.1 初见
假设上图就是我们的段(segment
)内存的保留内存(Reserve memory
)区域,那么你想到了什么?
是的,首先她是一个空荡荡的巨大空间!
当然,这里面也没有任何东西。
5.2 相识
现在,我们想要在段内存中保存一些东东,该怎么办呢?
是的,我们得混个脸熟!
好了,首先我们需要保存段的头信息,那让我们先提交个申请(通常是64K)。
有了第一次后,我们对这个操作流程应该熟门熟路了,所以,谁也抵挡不住我们前进的脚步。
再次提交存储对象的空间请求(通常是64K),当然,GC通常不会仅仅为一个对象申请内存.
5.3 行动
它通常先申请一个分配上下文,当然这个时候并没有对象被构造。
然后动用物理内存页,保存数据,查看存储信息如下:
1 | shell复制代码0:007> dq 000002470000c0c8-8 l3 |
其内部大致的流程如下(精简版):
注意:缓存是非常快的,以下是来自Intel的数据。
- L1 缓存: 4 cpu周期
- L2 缓存: 12 cpu周期
- L3 缓存: 44 cpu周期
DRAM的读取大约 60ns ~ 100ns之间。
5.4 小结下
经过前面不断的深入探索,对象的内存分布已经在你面前完全展开。那么,让我们再总结下。
GC的分配如下:
经过上面让人目眩神秘的命令和图片,你学废了吗?
最后,让我们打扫下战场,看看GC这位小宝贝。
6.1 GC怎么决定收集
如下代码,让我们看看它能有多智能?
1 | csharp复制代码public static int Main() |
发生了什么? 输出是:
Output - Collect called, h.Target is not collected
是的,你没有看错,GC.Collect()
收集整个堆栈,这意味着GC不能决定对象的生命周期。
如果一个对象还活着,那么GC会被告知,在这个例子中,JIT(User Roots)告诉GC,对象还活着。因此GC无法回收对象。
6.2 开始收集
好了,让我们来个真正的回收。
1 | csharp复制代码[MethodImpl(MethodImplOptions.NoInlining)] |
输出结果:
Output: Collect called, h.Target is collected
再次观察GC:
是的,GC摧毁了对象,内存回收了。
经过本次的多次深入刨析,你对你的对象是不是更加了解了?
👓都看到这了,还在乎点个赞吗?
👓都点赞了,还在乎一个收藏吗?
👓都收藏了,还在乎一个评论吗?
本文转载自: 掘金