在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在公共语言运行时 (CLR) 中,垃圾回收器 (GC) 用作自动内存管理器。 垃圾回收器管理应用程序的内存分配和释放。 对于使用托管代码的开发人员而言,这就意味着不必编写执行内存管理任务的代码。 自动内存管理可解决常见问题,例如,忘记释放对象并导致内存泄漏,或尝试访问已释放对象的内存。 本文章介绍垃圾回收的核心概念。 优点垃圾回收器具有以下优点:
内存基础知识下面的列表总结了重要的 CLR 内存概念。
内存分配初始化新进程时,运行时会为进程保留一个连续的地址空间区域。 这个保留的地址空间被称为托管堆。 托管堆维护着一个指针,用它指向将在堆中分配的下一个对象的地址。 最初,该指针设置为指向托管堆的基址。 托管堆上部署了所有引用类型。 应用程序创建第一个引用类型时,将为托管堆的基址中的类型分配内存。 应用程序创建下一个对象时,垃圾回收器在紧接第一个对象后面的地址空间内为它分配内存。 只要地址空间可用,垃圾回收器就会继续以这种方式为新对象分配空间。 从托管堆中分配内存要比非托管内存分配速度快。 由于运行时通过为指针添加值来为对象分配内存,所以这几乎和从堆栈中分配内存一样快。 另外,由于连续分配的新对象在托管堆中是连续存储,所以应用程序可以快速访问这些对象。 内存释放垃圾回收器的优化引擎根据所执行的分配决定执行回收的最佳时间。 垃圾回收器在执行回收时,会释放应用程序不再使用的对象的内存。 它通过检查应用程序的根来确定不再使用的对象。 应用程序的根包含线程堆栈上的静态字段、局部变量、CPU 寄存器、GC 句柄和终结队列。 每个根或者引用托管堆中的对象,或者设置为空。 垃圾回收器可以为这些根请求其余运行时。 垃圾回收器使用此列表创建一个图表,其中包含所有可从这些根中访问的对象。 不在该图表中的对象将无法从应用程序的根中访问。 垃圾回收器会考虑无法访问的对象垃圾,并释放为它们分配的内存。 在回收中,垃圾回收器检查托管堆,查找无法访问对象所占据的地址空间块。 发现无法访问的对象时,它就使用内存复制功能来压缩内存中可以访问的对象,释放分配给不可访问对象的地址空间块。 在压缩了可访问对象的内存后,垃圾回收器就会做出必要的指针更正,以便应用程序的根指向新地址中的对象。 它还将托管堆指针定位至最后一个可访问对象之后。 只有在回收发现大量的无法访问的对象时,才会压缩内存。 如果托管堆中的所有对象均未被回收,则不需要压缩内存。 为了改进性能,运行时为单独堆中的大型对象分配内存。 垃圾回收器会自动释放大型对象的内存。 但是,为了避免移动内存中的大型对象,通常不会压缩此内存。 垃圾回收的条件当满足以下条件之一时将发生垃圾回收:
托管堆在垃圾回收器由 CLR 初始化之后,它会分配一段内存用于存储和管理对象。 此内存称为托管堆(与操作系统中的本机堆相对)。 每个托管进程都有一个托管堆。 进程中的所有线程都在同一堆上为对象分配内存。 若要保留内存,垃圾回收器会调用 Windows VirtualAlloc 函数,并且每次为托管应用保留一个内存段。 垃圾回收器还会根据需要保留内存段,并调用 Windows VirtualFree 函数,将内存段释放回操作系统(在清除所有对象的内存段后)。
重要 垃圾回收器分配的段大小特定于实现,并且随时可能更改(包括定期更新)。 应用程序不应假设特定段的大小或依赖于此大小,也不应尝试配置段分配可用的内存量。 堆上分配的对象越少,垃圾回收器必须执行的工作就越少。 分配对象时,请勿使用超出你需求的舍入值,例如在仅需要 15 个字节的情况下分配了 32 个字节的数组。 当触发垃圾回收时,垃圾回收器将回收由非活动对象占用的内存。 回收进程会对活动对象进行压缩,以便将它们一起移动,并移除死空间,从而使堆更小一些。 这将确保一起分配的对象全都位于托管堆上,从而保留它们的局部性。 垃圾回收的侵入性(频率和持续时间)是由分配的数量和托管堆上保留的内存数量决定的。 此堆可视为两个堆的累计:大对象堆和小对象堆。 大对象堆包含大小不少于 85,000 个字节的对象,这些对象通常是数组。 非常大的实例对象是很少见的。 提示 可以配置阈值大小,以使对象能够进入大型对象堆。 代数GC 算法基于几个注意事项:
垃圾回收主要在回收短生存期对象时发生。 为优化垃圾回收器的性能,将托管堆分为三代:第 0 代、第 1 代和第 2 代,因此它可以单独处理长生存期和短生存期对象。 垃圾回收器将新对象存储在第 0 代中。 在应用程序生存期的早期创建的对象如果未被回收,则被升级并存储在第 1 级和第 2 级中。 因为压缩托管堆的一部分要比压缩整个托管堆速度快,所以此方案允许垃圾回收器在每次执行回收时释放特定级别的内存,而不是整个托管堆的内存。
当条件得到满足时,垃圾回收将在特定代上发生。 回收某个代意味着回收此代中的对象及其所有更年轻的代。 第 2 代垃圾回收也称为完整垃圾回收,因为它回收所有代中的对象(即,托管堆中的所有对象)。 幸存和提升垃圾回收中未回收的对象也称为幸存者,并会被提升到下一代:
当垃圾回收器检测到某个代中的幸存率很高时,它会增加该代的分配阈值。 下次回收将回收非常大的内存。 CLR 持续在以下两个优先级之间进行平衡:不允许通过延迟垃圾回收,让应用程序的工作集获取太大内存,以及不允许垃圾回收过于频繁地运行。 暂时代和暂时段因为第 0 代和第 1 代中的对象的生存期较短,因此,这些代被称为“暂时代”。 暂时代在称为“暂时段”的内存段中进行分配。 垃圾回收器获取的每个新段将成为新的暂时段,并包含在第 0 代垃圾回收中幸存的对象。 旧的暂时段将成为新的第 2 代段。 根据系统为 32 位还是 64 位以及它正在哪种类型的垃圾回收器(工作站或服务器 GC)上运行,暂时段的大小发生相应变化。 下表显示了暂时段的默认大小。
暂时段可以包含第 2 代对象。 第 2 代对象可使用多个段(在内存允许的情况下进程所需的任意数量)。 从暂时垃圾回收中释放的内存量限制为暂时段的大小。 释放的内存量与死对象占用的空间成比例。 垃圾回收过程中发生的情况垃圾回收分为以下几个阶段:
垃圾回收器使用以下信息来确定对象是否为活动对象:
在垃圾回收启动之前,除了触发垃圾回收的线程以外的所有托管线程均会挂起。 下图演示了触发垃圾回收并导致其他线程挂起的线程。 非托管资源对于应用程序创建的大多数对象,可以依赖垃圾回收自动执行必要的内存管理任务。 但是,非托管资源需要显式清除。 最常用的非托管资源类型是包装操作系统资源的对象,例如,文件句柄、窗口句柄或网络连接。 虽然垃圾回收器可以跟踪封装非托管资源的托管对象的生存期,但却无法具体了解如何清理资源。 创建封装非托管资源的对象时,建议在公共 还必须提供一种释放非托管资源的方法,以防类型使用者忘记调用
微软官方网址:https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals |
请发表评论