.Net 内存管理和垃圾回收(二)垃圾回收机制

本文是翻译Memory Management and Garbage Collection in .NET,本人英语水平不行,语文水平也不行,若有错误恳请评论指正。本文权当是英语翻译练习。


  1. .Net 内存管理和垃圾回收(一)非托管资源清除
  2. .Net 内存管理和垃圾回收(二)垃圾回收机制

垃圾回收(GC)基础

GC在CLR中充当一个自动的内存管理器,它有以下优点:

  • 使你可以开发应用程序而不需要你来释放内存(内存自动释放)。
  • 在托管堆上高效的分配对象所需内存。
  • 回收不再被使用的对象,清除它们占用的内存,在未来的分配中保存内存可用。托管对象在开始就自动获得干净的内容,所以它们不需要再构造函数(构造器)中初始化所有的数据字段。

1. 内存基础

以下列表总结了重要的CLR内存概念

  • 每一个 程序都有它自己独立的虚拟地址空间。所有在同一个计算机中的程序共享相同的物理内存,如果只有一个页文件的话也共享页文件。
  • 默认情况下,在32位计算机中,每一个程序拥有2GB的用户模式虚拟地址空间(如果在虚拟地址空间中使用win32函数,这些函数在本地堆【不是托管堆】中分配和释放虚拟内存)。
  • 虚拟内存有三种状态

    • Free(可用),内存块没有被引用,它可以被分配。
    • Reserved(保留),内存块可以被你使用,不可以用于其他任何的分配请求。但是,在它被Committed之前,你无法在此内存块中存储数据。
    • Committed(已提交),内存块被分配给物理存储器。
  • 虚拟地址空间可以被分割。这意味着在地址空间中存在被称为空洞的自由块。当请求虚拟内存分配时,虚拟内存管理器必须找到足够大的单个自由内存块来满足这个分配请求。即使你有2GB的自由空间,除非所有的自由空间在一个单独的地址块中,否则需要2GB的分配将会失败。

  • 如果虚拟地址空间使用完将会导致内存不足。

即使你的物理内存需求很低也会使用页面文件(pagefile.sys,存放虚拟内存的磁盘文件)。第一次你的物理内存需求很高,操作系统必须在物理内存中创建空间来存储数据,之后将部分在物理内存(主存)中的数据备份至页面文件(pagefile.sys,磁盘,辅存)。除非需要,数据将不会在页面文件中存储,在物理内存需求非常低的时将会遇到这种情况。

2. 垃圾回收的条件

当有一个以下的条件成立,将会引发垃圾回收:

  • 系统可用物理内存很低。检测到操作系统内存偏低通知或主程序表明内存很低。
  • 已使用的在托管堆上分配对象的内存超过可接受的阈值。此阈值在程序运行中动态调整。
  • GC.Collect方法被调用。在几乎所有的情况下,你没必要调用此方法,因为垃圾回收器在持续运行。此方法主要用于特定情况和测试。

3. 托管堆

垃圾回收器被CLR初始化之后,它会分配内存段去存储和管理对象。这部分内存被称为托管堆,相对于操作系统的本地堆(原生堆)。

这是每个托管进程的托管堆。所有在进程中的线程在同样的堆上分配内存。

垃圾回收器调用Win32的VirtualAlloc方法申请(预定/预留)内存,为应用程序一次申请一个内存段(segment)空间。垃圾回收器还会根据需要申请内存段,也会通过调用Win32的VirtualFree函数将内存段释放回操作系统(清除段内存里所有对象之后)。

垃圾回收器分配的内存段的大小是基于特点实现的,它可能随时调整,包括周期性的更新。你的应用程序不应该对特定的段大小做假设或依赖,或者尝试配置可用于段分配的内存量。

越少的对象被分配在堆上,垃圾回收器需要做的工作就越少。当你为对象分配内存时,不要使用往上舍入超出你需要的值,例如为需要15个byte的数组分配32个byte。

当垃圾回收被触发,垃圾回收器回收被已失效对象占用的内存。回收过程紧缩可用对象的内存,所以它们被移动在一起,无效的空间被移除,从而使得堆更小。这可以确保分配在一起的对象一起停留在托管堆上,保留其位置。


一起被分配的对象经常被一起使用,如果对象们在堆中的位置很紧凑的话,高速缓存的性能将会提高。


托管堆可以认为是两个堆的聚集:大对象堆和小对象堆。

大对象堆包含非常大的对象,85000个byte或者更大。在大对象堆上的对象通常是数组。非常大的实例对象是非常罕见的。


通常大对象具有很长的生命周期,大对象分配在大对象堆中,这部分堆永远不会被整理。因为移动大对象所带来的开销超过了整理这部分堆锁提高的性能。


4. 代(Generations)

托管堆被分成三种代,所以它可以处理长生命周期和短生命周期的对象。垃圾回收主要发生在回收短生命周期对象,通常只在很小的堆上发生。以下是在堆上的三种对象代:

  • 0代,这是最年轻的代,包含短生命周期对象。一个短生命周期的例子是临时变量。在这个段垃圾回收发生的最频繁。
    最新分配的对象构成一个新的对象代,暗含0代的集合,除非是大对象,在这种情况下它们进入2代集合的大对象堆。
  • 1代,这代包含短生命周期对象,被作为短生命周期和长生命周期的缓冲区。
  • 2代,这代包含长生命周期对象。一个长生命周期对象的例子是在服务程序中包含的在整个程序区间存在的静态数据对象。

根据条件,垃圾回收发生在特定的代。回收一个代意味着回收在此代中的对象和所有比此代低的所有代。2代回收也被认为是完全的垃圾回收,因为它回收了在所有代中的所有对象(所有在托管堆上的对象)。


代的引入主要是为了提升性能,以避免垃圾回收遍历整个堆。
每个GC周期都会检查第0代对象。大约1/10的GC周期检查第0代和第1代对象。大约1/100的GC周期检查所有的对象。


生存和提升

对象没有在垃圾回收中清除被认为是幸存者(活跃对象,引用计数不为0),将会被提升至下一段。从Generation 0 幸存下来的对象会提升至Generation 1,从Generation 1幸存下来的对象会被提升至Generation 2,从Generation 2幸存下来的对象会保留在Generation 2。

当垃圾回收器检测到在一个Generation中幸存的比例很高,它会为此Generation提升分配内存的阈值,所以下次回收会清除大量内存。CLR持续的在两个优先级中找到平衡:不让应用程序工作集太大和不让垃圾回收花费太多时间。

临时Generation(代)和Segment(段)

因为对象在Generation 0和1都存活很短,这些Generation被称为临时代。

临时代必须在临时段内存上分配。每一个被垃圾回收器获得的新段都将成为新的临时分段,包含0代垃圾回收后的幸存对象中。老的临时分段成为新的2代分段。

临时分段的大小取决于操作系统是32位还是64位,和运行的垃圾回收器类型。下表显示了默认值。

-32-bit64-bit
Workstation GC16 MB256 MB
Server GC64 MB4 GB
Server GC with > 4 logical CPUs32 MB2 GB
Server GC with > 8 logical CPUs16 MB1 GB

临时段可以包括2代对象。2代对象可以使用多个分段(只要你的程序需要和内存允许)。

从临时垃圾回收中释放的内存数量受临时段大小的限制。释放的内存数量与失效对象占用的空间成比例。

在垃圾回收期间发生了什么

一次垃圾回收有以下阶段(Mark Sweep法):

  • 标记阶段,查找并创建所有有效对象的列表
  • 重新定位阶段,更新将被紧缩对象的引用(在此阶段,将修改栈内的对象的引用地址/堆地址,可根据metadata知道每个对象占用的空间然后地址计算)
  • 紧缩阶段,清除失效对象占用的空间,紧缩幸存对象。紧缩阶段将垃圾回收幸存的对象移动至段的较旧的末端。(紧缩阶段会将有效的对象移动到堆的低地址,将失效对象移动到托管堆的高位置,并将NextObjPtr移动至最后一个有效对象的末尾地址)

因为2代集合会占有多个段,提升到2代的对象将会移动到一个较旧的段。1代和2代的幸存者可能被移动至不同的段,因为它们都被提升至2代。

一般来说,大对象堆不会被紧缩,因为拷贝大对象会对性能造成损失。然而,从.Net Framework 4.5.1开始,你可以使用GCSettings.LargeObjectHeapCompactionMode属性去根据你的需要紧缩大对象堆。

垃圾收集器使用一下的信息决定对象是否处于活动状态:

  • 堆栈根,JIT和堆栈助手提供的堆栈变量。
  • 垃圾回收句柄,用户代码或CLR分配的指向托管对象的句柄。
  • 静态数据,在应用程序域中可能引用其他对象的静态对象,每个应用程序域都会持续跟踪其静态对象。

在垃圾回收开始之前,除了触发垃圾回收的线程之外,所有的托管线程都会挂起。

下图展示一个线程触发了垃圾回收,从而导致其他线程被挂起:

操作非托管资源

如果你的托管对象通过使用其本地文件句柄引用了非托管对象,你必须明确的释放非托管对象,因为垃圾回收器只会跟踪托管堆内存。

你的托管对象的用户可能不会释放此对象使用的本地资源。为了执行清除,你可以使你的托管对象可终结。当对象不再使用时,完成执行由清理操作构成的析构函数。当你的托管对象死亡,它会执行在其终结器(析构函数)中指定的清除操作。

当一个可终结对象被发现死亡,其终结器已经放入一个队列,所以其清除操作会被执行,但是其自身对象将会被提升至下一代。因此,你必须等待发生在此代的下一次垃圾回收(不一定是下次回收,因为下次不一定会回收此代)来判定此对象是否被回收。

工作站和服务器垃圾回收机制

垃圾回收器是自适应的,可以工作在各种情况下。你可以使用配置文件设置根据工作负载的特点设置垃圾回收器的类型。CLR提供以下垃圾回收类型:

  • 工作站垃圾回收,适配所有的工作站和独立PC。这是在运行时配置架构中的gcServer元素默认配置。
    工作站垃圾回收器可以是并发或非并发的。并发垃圾回收使托管线程在垃圾回收期间能继续执行。
    从.NET Framework 4开始,后台垃圾回收替代了并行垃圾回收。

  • 服务器垃圾回收,致力于需要高吞吐和弹性的服务器应用。服务器垃圾回收可以使用非并发或后台的。

下图展示了在服务器中执行垃圾回收的专用线程。
服务器垃圾回收

配置垃圾回收

你可以使用运行时配置架构的gcServer元素指定你想要的CLR执行的垃圾回收类型。当此元素的‘enable’特性设置为‘false’(默认),CLR执行工作站垃圾回收。当你设置‘enable’特性为‘true’,CLR执行服务器垃圾回收。
并行垃圾回收通过运行时配置架构的gcConcurrent元素指定。默认设置时‘enable’。此设置控制并行和后台垃圾回收。
你也可以通过非托管主机接口指定服务器垃圾回收。如果你的应用程序托管在其中之一的环境,则ASP.NET和SQL Server会自动启用服务器垃圾回收。

比较工作站和服务器垃圾回收

以下是工作站垃圾回收线程和性能的注意事项:

  • 回收发生在触发垃圾回收的用户线程,并保持相同的优先级。因为用户线程通常以正常优先级运行,垃圾回收器(运行在正常优先级线程上)必须与其他线程竞争CPU时间。
    运行本地代码的线程将不会被挂起。
  • 工作站垃圾回收经常被用在只有一个处理器的计算机上,不管gcServer的配置。如果你指定了服务器垃圾回收,CLR使用禁用并发的工作站垃圾回收。

以下是服务器垃圾回收线程和性能的注意事项:

  • 垃圾回收发生在多个运行在THREAD_PRIORITY_HIGHEST优先级的专用线程上。
  • 为每一个CPU提供堆和专用线程来执行垃圾回收,同时堆被回收。每个堆包含一个小对象堆和一个大对象堆,所有堆都可以被用户代码访问。在不同堆的对象可以互相引用。
  • 因为多个垃圾回收线程工作在一起,在相同大小的堆上,服务器垃圾回收比要比工作站垃圾回收更快。
  • dd,
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值