DotNet垃圾回收器及内存管理机制
内存基础知识
- 每个进程都有自己单独的虚拟地址空间。同一台计算机上所有的进程共享相同的物理内存和页文件
- 默认情况下,32位计算机上的每个进程都具有2GB的用户模式虚拟地址空间
- 在公共语言运行时CLR中,垃圾回收器用作自动内存管理器
- 作为开发人员,只能使用虚拟地址空间,请勿直接操控无力内存。垃圾回收器为你分配和释放托管堆上的虚拟内存
- 虚拟内存分为三种状态
- Free,可用(即可分配)
- 保留(即已分配)
- 已提交(即已指派给物理内存)
- 可能会存在虚拟地址空间碎片。就是说地址空间中存在一些被称为孔的可用块。当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。即使有2GB可用空间,2GB分配请求也会失败,除非所有这些可用空间都位于一个地址块中
- 如果没有足够的可供保留的虚拟地址空间或可供提交的物理空间,则可能会用尽内存
- 发生垃圾回收的三种情况(垃圾回收的条件)
- 物理内存不够了
- 托管堆上已分配的对象使用的内存过高
- GO.Collect()被调用
- 在垃圾回收器CLR初始化之后,它会分配一段内存用于存储和管理对象,此内存称为托管堆(与操作系统中的本机堆相对),一个用于小型对象(小型对象堆或SOH,small object heap),一个用于大型对象(大型对象堆)。每个托管进程都有一个托管堆,进程中的所有线程都在同一个堆上给对象分配内存。
- 托管堆视为两个堆的累计:大对象堆和小对象堆
- 垃圾回收器分配的段大小是动态的
- 堆以代数(共3代,第0代,第1代,第2代)来组织,这样就能有效管理短生存对象和长生存对象。垃圾回收最常发生在第0代(包含短生存期对象),当回收第2代时会连带它所有代一起回收。垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代
- 垃圾回收过程中发生了什么:
- 标记阶段,找到并创建所有活动对象的列表
- 重定位阶段,用于更新堆将要压缩的对象的引用
- 压缩阶段,用于回收由死对象占用的空间,并压缩幸存的对象
- 垃圾回收器使用以下信息来确定对象是否为活动对象(是否回收):
- 堆栈根。
- 垃圾回收句柄。指向托管对象且可由用户代码或公共语言运行时CLR分配的句柄
- 静态数据。应用程序域中kennel引用其他对象的静态对象,每个应用程序域都会跟踪气静态对象
- 垃圾回收启动之前,出了触发垃圾回收的线程以外的所有托管线程均会被挂起
代数
GC算法基于几个注意事项:
- 压缩托管堆的一部分内存要比压缩整个托管堆速度快
- 较新的对象生存周期较短,而较旧的对象生存周期则较长
- 较新的对象趋向于互相关联,并且大致同时由应用程序访问
垃圾回收主要在回收短生存期对象时发生。为优化垃圾回收器的性能,将托管堆氛围三代:第0代,第1代和第2代,因此它可以单独处理长生存期和短生存期对象。垃圾回收器将新对象存储在第0代中。在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第1级和第2级中,因为压缩托管堆的一部分要比压缩整个托管堆速度快,所以此方案允许垃圾回收器在每次执行回收时释放特定级别的内存,而不是整个托管堆的内存。
-
第0代。
这是最年轻的代,其中包含短生存期对象。短生存期对象的一个示例是临时变量。垃圾回收最长发生在此代中。
新分配的对象构成新一代对象,并且隐式地称为第0代集合。但是,如果它们是大型对象,它们将延续到大型对象堆LOH,这有时称为第3代。第3代是在第2代中逻辑收集的物理生成。
大多数对象通过第0代中的垃圾回收进行回收之后不会保留到下一代
-
第1代
这一代包含短生存期对象并作用短生存期对象和长生存期对象之间的缓冲区。
垃圾回收器执行第0代托管堆的回收后,会压缩可访问对象的内存,并将其升级到第1代。因为违背回收的对象往往具有较长的生存期,所以将它们升级到更高的级别很有意义。垃圾回收器不必在每次执行第0代托管堆的回收时,都重新检查第1代和第2代托管堆中的对象。
如果第0代托管堆的回收没有回收足够的内存供应用程序创建新对象,垃圾回收器就会先执行第1代托管堆的回收,然后再执行第2代托管堆的回收。第1级托管堆汇总违背回收的对象会升级到第2级托管堆。
-
第2代
这一代包含长生存周期对象。长生存期对象的一个示例是服务器应用程序中的一个包含在进程期间处于活动状态的静态数据的对象。
第2代托管堆中未被回收的对象会继续保留在第2代托管堆中,直到在将来的回收中确定它们无法访问位置。
大型对象堆上的对象(有时候也称为第3代)也在第2代中收集。
当条件得到满足时,垃圾回收将在特定代上发生。回收某个代意味着回收此代中的对象及其所有梗年轻的代。
第2代垃圾回收也称为完成垃圾回收,因为它回收所有代汇总的对象(即,托管堆中的所有对象)。
CLR 持续在以下两个优先级之间进行平衡:不允许通过延迟垃圾回收,让应用程序的工作集获取太大内存,以及不允许垃圾回收过于频繁地运行。
内存分配
初始化新进程时,运行时会为进程保留一个连续的地址空间区域。这个保留的地址空间被称为托管堆。托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。最初,该指针设置为指向托管堆的基址。托管堆上部署了所有引用类型。应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。
从托管堆中分配内存要比非托管内存分配速度快。由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。另外,由于连续分配的新对象在托管堆中是连续存储的,所以应用程序可以快速访问这些对象
大型对象堆LOH(Large Object Heap)
如果对象大于或等于 85,000 字节,将被视为大型对象。 此数字根据性能优化确定。 对象分配请求为 85,000 字节或更大时,运行时会将其分配到大型对象堆。
小型对象始终在第 0 代中进行分配,或者根据它们的生存期,可能会提升为第 1 代或第 2 代。 大型对象始终在第 2 代中进行分配。大型对象属于第 2 代,因为只有在第 2 代回收期间才能回收它们。 回收一代时,同时也会回收它前面的所有代。
执行GC的情形
-
分配超出第0代或大型对象阀值
阈值是某代的属性。 垃圾回收器在其中分配对象时,会为代设置阈值。 超出阈值后,会在该代上触发 GC。
这是典型情况,大部分 GC 执行都因为托管堆上的分配。
-
调用GC.Collect()方法
如果调用五参数GC.Collection()方法,或另一个重载作为参数传递到GC.MaxGeneration,将会一起收集LOH和剩余的托管堆
-
系统处于内存不足的状况
垃圾回收器收到来自操作系统的高内存通知时,会发生以上情况。如果垃圾回收器认为执行第二代GC会有小了,它将出发第二代
非托管资源
对于应用程序创建的大多数对象,可以依赖垃圾回收自动执行必要的内存管理任务。但,非托管资源需要显示清楚。最常用的非托管资源类型是包装操作系统资源的对象,如,文件句柄,窗口句柄或网络连接。
创建封装非托管资源的对象时,建议在公共Dispose方法中提供必要的代码以清理非托管资源。