1. Stack和Heap
每一个线程对应一个stack,线程建立的时候CLR为其建立这个stack,stack主要做用是记录函数的执行状况。值类型变量(函数的参数、局部变量 等非成员变量)都分配在stack中,引用类型的对象分配在heap中,在stack中保存heap对象的引用指针。GC只负责heap对象的释 放,heap内存空间管理
Heap内存分配
除去pinned object等影响,heap中的内存分配很简单,一个指针记录heap中分配的起始地址,根据对象大小连续的分配内存
Stack结构
每一个函数调用时,逻辑上在thread stack中会产生一个帧(stack frame),函数返回时对应的stack frame被释放掉
用个简单的函数查看执行时CLR对栈的处理状况:算法
JIT编译后主要汇编代码以下(其余的状况下汇编代码可能有所差异,但用这个简单函数大体看下栈的管理已经足够):缓存
执行时刻的stack状态以下(栈基地址为高端地址,栈顶为低端地址):
Stack状态变化过程:
a). 调用者将第三、第四、第5个参数压栈,第一、第2个参数分别放入ecx、edx寄存器
b). call指令调用函数Sum,并自动将函数返回地址压栈,代码跳转到函数Sum开始执行
c). 函数Sum先将寄存器ebp压栈保存,并将esp放入ebp,用于后面对参数和局部变量寻址
d). 定义局部变量以及省略掉的是额外代码,跟Sum函数业务无关
e). 执行加法操做,结果保存在eax寄存器中
f). 恢复esp寄存器,这样函数Sum中全部的局部变量以及其余压栈操做所有释放出来
g). 原始ebp的值出栈,恢复ebp,这样栈彻底恢复到进入Sum函数调用时的状态
h). ret指令执行函数返回,返回值在eax寄存器中,返回地址为call指令压栈的地址,返回地址自动出栈。0Ch指示处理器在函数返回时释放栈中12个字 节,即由被调用者清除压栈的参数。函数返回以后,本次Sum调用的栈分配所有释放
这种调用约定相似__fastcall
结合引用类型变量、值类型的ref参数,下面代码简化的stack状态以下:
代码:
服务器
Stack状态:
任什么时候候引用类型都分配在heap中,在stack中只是保存对象的引用地址。Run函数执行完毕以后,heap中的MyClass1对象c成为可回收的垃圾对象,在GC时进行回收
2. Mark-Compact 标记压缩算法
简单把.NET的GC算法看做Mark-Compact算法
阶段1: Mark-Sweep 标记清除阶段
先假设heap中全部对象均可以回收,而后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是能够被回收的
阶段2: Compact 压缩阶段
对象回收以后heap内存空间变得不连续,在heap中移动这些对象,使他们从新从heap基地址开始连续排列,相似于磁盘空间的碎片整理
Heap内存通过回收、压缩以后,能够继续采用前面的heap内存分配方法,即仅用一个指针记录heap分配的起始地址就能够
主要处理步骤:将线程挂起=>肯定roots=>建立reachable objects graph=>对象回收=>heap压缩=>指针修复
能够这样理解roots:heap中对象的引用关系错综复杂(交叉引用、循环引用),造成复杂的graph,roots是CLR在heap以外能够找到的 各类入口点。GC搜索roots的地方包括全局对象、静态变量、局部对象、函数调用参数、当前CPU寄存器中的对象指针(还有finalization queue)等。主要能够归为2种类型:已经初始化了的静态变量、线程仍在使用的对象(stack+CPU register)
Reachable objects:指根据对象引用关系,从roots出发能够到达的对象。例如当前执行函数的局部变量对象A是一个root object,他的成员变量引用了对象B,则B是一个reachable object。从roots出发能够建立reachable objects graph,剩余对象即为unreachable,能够被回收
指针修复是由于compact过程移动了heap对象,对象地址发生变化,须要修复全部引用指针,包括stack、CPU register中的指针以及heap中其余对象的引用指针
Debug和release执行模式之间稍有区别,release模式下后续代码没有引用的对象是unreachable的,而debug模式下须要等到 当前函数执行完毕,这些对象才会成为unreachable,目的是为了调试时跟踪局部对象的内容
传给了COM+的托管对象也会成为root,而且具备一个引用计数器以兼容COM+的内存管理机制,引用计数器为0时这些对象才可能成为被回收对象
Pinned objects指分配以后不能移动位置的对象,例如传递给非托管代码的对象(或者使用了fixed关键字),GC在指针修复时没法修改非托管代码中的引用 指针,所以将这些对象移动将发生异常。pinned objects会致使heap出现碎片,但大部分状况来讲传给非托管代码的对象应当在GC时可以被回收掉
3. Generational 分代算法
程序可能使用几百M、几G的内存,对这样的内存区域进行GC操做成本很高,分代算法具有必定统计学基础,对GC的性能改善效果比较明显
将对象按照生命周期分红新的、老的,根据统计分布规律所反映的结果,能够对新、老区域采用不一样的回收策略和算法,增强对新区域的回收处理力度,争取在较短 时间间隔、较小的内存区域内,以较低成本将执行路径上大量新近抛弃再也不使用的局部对象及时回收掉
分代算法的假设前提条件:
a). 大量新建立的对象生命周期都比较短,而较老的对象生命周期会更长
b). 对部份内存进行回收比基于所有内存的回收操做要快
c). 新建立的对象之间关联程度一般较强。heap分配的对象是连续的,关联度较强有利于提升CPU cache的命中率
.NET将heap分红3个代龄区域: Gen 0、Gen 一、Gen 2
Heap分为3个代龄区域,相应的GC有3种方式: # Gen 0 collections, # Gen 1 collections, # Gen 2 collections。若是Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen 1。若是Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一块儿进行回收,幸存的对象进入Gen 2。2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一块儿回收
Gen 0和Gen 1比较小,这两个代龄加起来老是保持在16M左右;Gen 2的大小由应用程序肯定,可能达到几G,所以0代和1代GC的成本很是低,2代GC称为full GC,一般成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时full GC可能须要花费几秒时间。大体上来说.NET应用运行期间2代、1代和0代GC的频率应当大体为1:10:100
图为一个ASP.NET程序运行的Performance Moniter,Gen 0 heap size(红色)平均6M,Gen 1(蓝色)平均5M,Gen 2(黄色)达到620M,Gen 0+Gen 1平均13.2M,最大19.8M
直观上来看,程序的运行由一系列函数调用组成,函数运行期间会建立不少局部对象,函数结束以后也就产生大量待回收的对象。采用分代算法增强较新代龄的垃圾 回收力度,一般可以极大的提升垃圾回收效率,不然就是极特殊的程序,或者是不合理的对象关联设计。例如ASP.NET程序,应当确保绝大部分用于HTTP 请求处理的对象在0代和1代垃圾回收中被释放掉
为heap记录几个指针能够肯定代龄区域范围,建立reachable objects graph时根据对象的地址能够肯定对象位于哪一个代龄区域,0代GC在建立graph时若是遇到1代、2代heap对象,能够直接越过不用继续遍历下去, 较老代龄的对象若是引用了较新代龄的对象,能够经过Win32 API GetWriteWatch订阅内存更新通知,记录在"card table"中,辅助较低代龄的GC正确构造graph
4. LOH
.NET 1.1和2.0中,85000字节如下的对象称为小对象,分配在Gen 0 heap中,85000字节以上的对象称为大对象,分配在Large Object Heap中,这是由于GC在heap压缩时移动大的内存块须要消耗大量CPU时间,经过性能调优实践肯定了85000字节这样一个阀值
LOH只在2代GC时进行回收,采用Mark-Sweep算法,没有压缩处理,所以LOH中的内存分配是不连续的,使用一个空闲列表free list记录LOH中的空闲空间,对释放出来的空间进行管理
上图中obj一、obj2释放以后,其空间合并起来成为free list的一个节点,随后被分配给obj4
何时触发垃圾回收?
前面已经提到,0代和1代垃圾回收主要由阀值控制。初始时Gen 0 heap大小与CPU缓存的大小相关,运行时CLR根据内存请求状态动态调整Gen 0 heap大小,但Gen 0和Gen 1总大小保持在16M左右
Gen 2 heap和LOH都在full GC时进行回收,full GC主要由2类事件触发:
a). 进入Gen 2 heap和LOH的对象不少,超过了必定比例。RegisterForFullGCNotification的参数 maxGenerationThreshold、largeObjectHeapThreshold能够分别为Gen 2 heap和LOH设定这个值
b). 操做系统内存吃紧的时候。CLR会接收到操做系统内存紧张的通知消息,触发full GC
5. Heap细节、扩容与收缩
Heap的代龄是逻辑上的结构,heap实际内存申请和分配以及释放以segment(段)为单位,workstation GC模式segment大小为16M,server GC模式segment大小为64M。Gen 0和Gen 1 heap老是位于同一个段中,叫作ephemeral segment(新生段),所以max(Gen 0 heap size+Gen 1 heap size)≈16M || 64M,Gen 2 heap由0个或多个segments组成,LOH由1个或多个segments组成
.NET程序启动时CLR为heap建立2个segment,一个做为ephemeral segment,另外一个用于LOH。.NET使用VirtualAlloc申请和分配heap内存,在LOH中分配新对象时没有足够的空间,或者1代GC 时进入Gen 2的对象过多空间不够,.NET将为LOH或者小对象heap分配新的segment。申请新的segment失败将由EE抛出OutOfMemory异 常
Full GC后彻底空闲的segments将被释放掉,内存返回给操做系统
.NET 2.0对GC的一个重要改进是尽可能改善heap碎片处理。heap碎片主要由pinned objects引发,改善措施主要有2个方面。首先是延迟升级,若是ephemeral segment存在pinned objects,则尽量的延迟他们升级到Gen 2的时间点,考虑pinned objects的同时尽可能充分利用当前ephemeral segment的空间;其次是重复利用Gen 2的空间,若是Gen 2中存在pinned objects的segments释放出了足够空间,该segments可能从新做为ephemeral segment使用
6. GC方式
有Workstation GC with Concurrent GC off、 Workstation GC with Concurrent GC on、Server GC 3种
Workstation GC with Concurrent GC off: 用于单CPU机器实现高吞吐量,采用一系列策略观察内存分配以及每次GC的情况,动态调整GC策略,尽量使程序随着运行时状态的变化实现高效的GC操 做,但进行GC时会冻结全部线程
Workstation GC with Concurrent GC on: 用于响应时间很是重要的交互式程序,例如流媒体的播放等(若是一次full GC致使应用程序中断几秒、十几秒时间,用户将没法忍受)。这种方式利用多CPU对full GC进行并行处理,不是整个full GC期间冻结全部线程,而是将full GC切分红屡次很短的时间对线程进行冻结,在线程冻结时间以外,应用程序仍然能够正常运行,进行内存分配,这主要经过将Gen 0 heap size设置的比non-concurrent GC大不少而实现,使得GC操做时线程仍然可以在Gen 0 heap中进行内存分配,但若是Gen 0 heap用完后GC仍然没有结束,线程仍然会出现阻塞。这种方式付出的代价是working set和GC所需时间比non-concurrent GC要大一些
Server GC: 用于多CPU机器的服务器应用程序实现高吞吐量和伸缩性,充分利用服务器的大内存。.NET为每一个CPU建立一组heap(包括Gen 0, 1, 2和LOH)和一个GC线程,每一个CPU能够独立的为相应的heap执行GC操做,而其余CPU则正常执行处理。最佳的应用场景是多线程之间内存结构基本 相同,执行的工做相同或相似
单CPU机器上只能使用workstation GC,默认状况下为Workstation GC with Concurrent GC on方式,单CPU机器上配置为Server GC无效,仍然使用workstation GC;多CPU服务器上的ASP.NET默认使用Server GC方式,Server GC时不能使用concurrent方式
concurrent GC能够用于单CPU机器,它与CPU数量无关
对于ASP.NET程序应当尽可能保证一个CPU仅对应一个GC线程,防止同一个CPU上面多个GC线程之间的冲突形成性能问题。若是使用了Web Garden则应当使用Workstation GC with Concurrent GC off。Web Garden为了提升吞吐量会致使多出几倍的内存使用,每一个work process的内存有不少重复部分,Web Garden的最佳应用场景是多个进程之间使用一个共享的resource pool,避免内存的重复并尽量的提升吞吐量。在这一点上Server GC应当与Web Garden相似,但Web Garden在多个进程中,而Server GC是在同一个进程中经过多线程实现,目前没有发现Server GC方面深刻一些的资料,不少东西只能根据现有资料作一些猜测
为workstation GC禁用concurrent GC:
多线程
启用Server GC:
异步
7. Finalization
具备finalize method的对象在垃圾回收时,.NET先调用finalize method,而后再进行回收,具体处理以下:
a). 在heap建立具备finalize method的对象时,对象指针会放入finalization queue;
b). 垃圾回收时,具备finalize method的对象若是成为unreachable,则将其指针从finalization queue中移除,放入freachable queue,在本次垃圾回收处理中并不对这些对象进行回收;其它没有finalize method的unreachable对象正常回收。freachable queue中的对象是reachable的(它引用到的其余对象也都是reachable的)
c). 垃圾回收结束后,若是freachable queue非空,则一个专门的运行时线程finalizer thread被唤醒,它逐个调用freachable queue中对象的finalize method,而后将其指针从freachable queue中移除
d). 通过步骤c的处理以后,第二次垃圾回收时这些对象就成为unreachable,被正常回收
由于finalize method被设计用于非托管资源的释放,对这些资源的释放可能须要较长的时间,为了优化垃圾回收处理的性能,所以将调用finalize method专门交给一个独立的线程finalizer thread异步进行处理,这样也形成finalize method的对象须要通过2次垃圾回收处理
参考:
Garbage Collection - Past, Present and Future, Patrick Dussud, 中文翻译: .NET垃圾收集器的过去如今和将来(一), (二)
C# Heap(ing) Vs Stack(ing) in .NET Part I, Part II, Part III, Part IV Matthew Cochran
Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework Jeffrey Richter
Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework Jeffrey Richter
CLR Inside Out: Large Object Heap Uncovered Maoni Stephens
Heap: Pleasures and Pains Murali R. Krishnan
The Dangers of the Large Object Heap Andrew Hunter
Garbage Collection Notifications
Garbage Collector Basics and Performance Hints Rico Mariani
CLR Inside Out: Investigating Memory Issues Claudio Caldato and Maoni Stephens
Understanding Garbage Collection in .NET Andrew Hunter
Using GC Efficiently Part 1, Part 2, Part 3, Part 4 Maoni Stephens
Notes on the CLR Garbage Collector Vineet Gupta
The Mystery of Concurrent GC Mark Smith
Garbage Collection Curriculum Ferreira Paulo, Veiga Luís
Java theory and practice: A brief history of garbage collection Brian Goetzide