带着问题去思考!大家好
相对.NET 来说。CLR去处理了,C,C++这些就需要手动去垃圾回收。
GC大部分容易察觉的性能问题。其实很多问题实际是哪个都是由于对垃圾回收器的行为和预期结果理解有误。在,NET环境中,你需要更多的关注内存的性能,那么接下里我们主要是讲内存性能问题。
GC实际上会调整体提高内存堆[1]的性能,因为他能高效的完成内存分配和碎片整理工作。
在Windows的本机代码模式下,内存堆维护着一张空闲内存块的列表,用于内存的分配,尽量用低碎片化的内存堆,因为长时间运行,还要考虑内存碎片问题,内存占用率会持续增长。所以本机代码程序用大量代码实现了自己的内存分配机制,把默认的malloc函数给替换掉
在.NET环境中,内存分配的工作量会很小,因为内存总是整段分配的。所以不会比内存的扩大,减小和比较增加多少开销,不存在遍历空闲内存列表,几乎不会出现内存碎片,GC内存堆的效率还会更高的,因为连续分配的多个对象往往在内存堆中也是连续存放,提高就近访问的可能性。
在默认的内存分配流程中,有一小段代码会先检查目标对象的大小,看看内存分配缓冲区中所剩的内存够不够,如果内存分配缓冲区已耗尽,就会交由GC分配程序来检索足以容纳目标对象的空闲内存。然后一个新的分配缓冲区就会被保留下来。
接下里看下内存分配的过程
class MyObject { int x; int y; int z; } static void Main(string[] args) { var x = new MyObject(); }
了解汇编语言的都知道这一流程,
MOV指令是数据传送指令,其他的大家可以自行去查约。大致是说,把类的方法表指针拷贝到ecx(计数暂存器)中,作为new ()的参数,调用new,把返回值(对象的地址)拷贝到寄存器中。这里大家大概了解一下就可以了
基本运作方式
在托管进程中存在两种内存堆(本机堆和托管堆),这里我们说下托管堆,本机堆大大家可以自行了解下,
CLR在托管堆(Managed Heap)上为所有的.NET托管对象分配内存,也称之为GC堆,因为其中的对象都要受到垃圾回收机制的控制。
托管堆又分为两种,小对象堆和大对象堆(LOH),都拥有自己的内存段(Segment[ˈseɡmənt]).内存段的大小视配置和硬件环境而定。
小对象堆
小对象堆有什么?它又是怎么变化的?
小对象堆的内存堆分为3代,0,1,2代。第0代和第1代总是位于同一个内存段中,第2代可能跨越多个内存段,LOH也可以跨越多个内存段。包含第0代和第1代堆的内存段被称为暂时段(Ephemeral [ɪˈfemərəl] Segment ˈseɡmənt])
小对象堆中分配内存的对象生存期。如果 对象小于85000字节,CLR会把它分配在小对象堆中的第0代,通常紧挨当前已用内存空间往后分配,如果扩大内存堆时超越了内存段的边界,则就会触发垃圾回收过程。
对象总是诞生于第0代内存堆中,只要对象保持存活,每当发生垃圾回收时,GC都会把他提升一代。第0代和第1代内存堆的垃圾回收有时候被称为瞬时回收(Ephemeral Collection)
在垃圾回收的时候,可能会进行碎片整理(Compaction),也及时GC把对象物理迁移到新的位置中去,以便让内存段中的空闲空间能够连续起来使用。如果为发生碎片整理,那就只需要重新调整各块内存的边界。以下是经历几次未做碎片整理的垃圾回收之后,内存堆的分布可能如下
对象的位置没有移动,但是各代的内存堆的边界发生了变化。
如果对象到达第2代内存堆,它就会一直留在哪里直至终结。但不代表第2代内存堆只会一直变大, 如果第2代内存堆中的对象都终结了,整个内存段有没有存活[2]的对象,垃圾回收器会把整个内存段交换给操作系统,或者作为其他几代内存堆的附加段,在进行完全垃圾回收(Full Garbge Collection)时,就会可能发生第2代内存堆的回收。
[2]:存活:GC能通过任一已知的GC跟对象(Root),沿着层层引用访问到某个对象,那么它是存活的。GC的根对象可以是程序中的静态变量,或者某个线程的堆栈被正在运行的方法占用(局部变量)或者GC句柄(比如固定对象的句柄,Pinned Handle),或是终结器队列(Finalizer Queue),有些对象可能没有受GC根对象的引用,但如果位于第2代内存堆中,那么第0代回收是不会清理这些对象的。只有完全垃圾回收才会被清理。
如果第0代堆即将占满一个内存段,垃圾回收也无法通过碎片整理获取足够空闲内存,那么GC会分配一个新的内存段。如图:
如果第2代堆继续变大,就可能会跨越多个内存段。LOH也是。但是无论存在多少内存段,第0代和第1代总是位于同一个内存段中。以后我们找出内存堆中有哪些对象存活时。这些会用到。
LOH
LOH,大于85000字节的对象将自动在LOH中分配内存。没有代的概念,垃圾回收期间也不会自动进行碎片化整理,但是可以人为的碎片整理。
垃圾回收的时候会造成什么影响呢?
垃圾回收是针对某一代及以下几代内存堆进行的。如果回收1代,就会回收0代。如果发生了第0代或第1代垃圾回收,程序在回收期间就会暂停运行,第二代垃圾回收,有部分回收是在后台线程运行进行的。
垃圾回收的4个阶段
- 挂起(Suspension)---在垃圾回收发生之前,所有托管线程都被强行中止
- 标记(Mark)--从GC根对象开始,垃圾回收器沿着所有对象引用进行遍历并把所见对象记录下来
- 碎片整理(Compact)--将对象重新紧挨着存放并更新所有引用,以便减少内存碎片,在小对象堆中,碎片整理会按需进行,无法控制。在LOH中,碎片整理不会自动进行,可以必要时通知垃圾回收器来上一次。
- 恢复(Resume)--托管线程恢复运行
在标记阶段,不需要遍历内存堆中的所有对象,只要访问那些需要回收的部分即。比如第0代回收只涉及到第0代内存堆中的对象,第1代回收将会标记第0代和第1代内存中的对象。第2代和完全回收,需要遍历内存堆中的所有存活对象,这一开销很大。
1:垃圾回收过程的耗时几乎完全取决于所涉及的“代”内存堆中的对象数量,而不是你分配到的对象数量,你分配1棵包含100万个对象的树,只要在下次垃圾回收之前把根对象的引用解除。就不会增加垃圾回收的耗时
2:只要已分配的内存超过某个内部阀值,就会发生这个代垃圾回收,这个阀值是持续变化的。GC会进行调整。
计数器