CoreCLR文档翻译 - GC的设计

此文档来源于CoreCLR的BOTR(The Book of the Runtime), 点击打开原文
一切著作权归微软公司所有

GC的设计

作者: Maoni Stephens (@maoni0) - 2015

提示: 推荐看 The Garbage Collection Handbook 这本书学习更多关于GC的知识 (在文章底部的链接中)

组件结构

在GC中有两个主要的组件, 一个是分配器(Allocator), 另一个是收集器(Collector).
分配器负责获取更多的内存并且在适当的时机触发收集器.
收集器负责回收垃圾和不再被程序使用的对象内存.

此外还有一些途径可以触发收集器, 例如手动调用GC.Collect函数或析构线程(Finalizer Thread)收到一个内存不足的异步通知(由收集器发送).

分配器的设计

分配器由运行引擎(Execution Engine (EE))调用, 调用时会带有以下的信息:

  • 需要分配的大小

  • 线程专用的分配上下文(Allocation Context)

  • 标记, 如对象是否有析构函数

GC不会根据对象类型的不同做出特殊处理, 它会从运行引擎获取对象的大小, 根据对象的大小把对象分为两类:

  • 小对象 (小于 85,000字节)

  • 大对象 (大于或等于 85,000字节)

原则上小对象和大对象都可以用同样的方式处理, 但因为压缩(Compacting)大对象的代价会比较昂贵所以GC会区分对待.

当GC把一段内存交给分配器时, 它会参照分配上下文(Allocation Context).
分配上下文的大小取决于分配单位(Allocation Quantum)的定义.

  • 分配上下文是在堆段(Heap Segment)中专门给指定线程使用的小区域, 在单核计算机(指单逻辑核心)中只会使用一个上下文, 即第0世代(Generation 0)分配上下文.

  • 分配单位是分配器每次申请的内存大小, 用于在分配上下文中给对象分配内存. 分配单位通常为8K且受管理对象(Managed Object)的大小通常为32字节, 使得一个分配单位可以用于多个对象的分配 (译者注: 原理和内存池相同)

大对象不会使用分配上下文和分配单位, 因为一个大对象本身可以大于这些小区域.
并且使用这些区域带来的好处(会在下面讨论)仅仅受限于小对象.
大对象会直接在堆段(Heap Segment)中分配.

分配器的设计要求实现:

  • 在适当时机触发GC: 如果超过了分批预算(由收集器设置的阈值)或分配器不能在指定的堆段上分配时将会触发GC, 分配预算(Allocation Budget)和受管理的段(Managed Segments)的细节将在下面讨论.

  • 保持对象位置: 如果多个对象在同一个堆段中分配, 它们的虚拟内存地址也会邻接.

  • 高效的使用缓存: 分配器每次都会按 分配单位 申请内存, 而不是按每个对象的大小. 在申请内存后它会对足够激活cpu缓存的内存大小进行清0, 因为在此之后对象会从这块内存中分配(所以性能会得到提升). 分配单位通常为8K.

  • 高效的避免线程锁: 因为分配上下文和线程的绑定, 可以保证每个分配单位的内存只有对应一个线程可以操作. 因此只要当前的分配上下文没有用完, 给对象分配内存时就不需要线程锁.

  • 内存完整性: GC总会把新分配的对象的内存清0, 从而防止对象指向随机的内容(未定义的内容).

  • 保证堆可爬取(Crawlable): 分配器会在每个分配单位即将用完之际新建一个自由对象(Free Object)填充, 例如一个分配单位剩余30个字节并且下一个要分配的对象需要40个字节, 则分配器会新建一个30个字节的自由对象填充原来的分配单位并重新获取一个新的分配单位

分配器的API

 Object* GCHeap::Alloc(size_t size, DWORD flags);Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD flags);

以上的函数可以用于分配小对象和大对象.
另外还有一个函数用于强制从大对象的堆(LOH: Large Object Heap)中分配内存:

 Object* GCHeap::AllocLHeap(size_t size, DWORD flags);

收集器的设计

GC的目标

GC致力于高效的内存管理, 只要求程序员付出很小的努力
高效指的是:

  • GC应该足够频繁的发生, 以避免堆中有大量(根据比例和绝对值)未使用但已分配的对象(垃圾)和多余的内存.

  • GC应该尽可能不频繁的发生, 以避免过多的消耗cpu时间, 即便频繁发生可以让程序占用更小的内存.

  • GC应该是生产性的, 如果一次GC只回收了少量的内存那么这次GC和它消耗的cpu周期被浪费了.

  • 每次GC应该足够快, 许多场景会要求低延迟.

  • 程序员们不需要为了优化内存的利用对GC了解太多(取决于他们的工作).
    – GC应该调整自身以适应不同的内存使用模式.

受管理的堆(Managed Heap)的逻辑表现

CLR GC把对象分成了不同的世代, 当第 N 世代的垃圾被回收后, 生存的对象会被标记为第 N+1 世代, 这个过程被称为升级(Promotion).
还有一些例外的情况当我们决定是否降级或不升级.

对于小对象的堆会分为3个世代: 第0世代(gen0), 第1世代(gen1)和第2世代(gen2).
对于大对象只有1个世代: 第3世代(gen3).
第0世代和第1世代被称为短暂(对象的生命周期短)的世代.

对于小对象的堆, 世代中的数字代表了年龄, 第0世代是最年轻的世代.
但不代表第0世代中的对象一定比第1世代和第2世代的对象年轻, 下面将会说明那些例外.
收集一个世代中的垃圾同时也会收集所有比它年轻的世代的垃圾.

原则上大对象可以使用和小对象一样的处理方式, 但是压缩大对象的代价会非常的昂贵, 因此大对象和小对象会受到不同的对待.
因为性能上的原因, 大对象只使用了一个世代(第3世代)并且这个世代会和第2世代一起回收垃圾.
因为第2世代和第3世代可以很大, 需要和短暂的世代(第0世代和第1世代)在开销上划出边界.

为对象分配内存时总会从最年轻的世代分配 - 对于小对象总会从第0世代分配, 对于大对象总会从第3世代分配(因为只有一个世代).

受管理的堆(Managed Heap)的物理表现

受管理的堆(Managed Heap)是一个包含了受管理的堆段(Managed Heap Segments)的集合.
受管理的堆段是从系统内核申请得到的一块连续的内存空间. (译者注: 即malloc/brk申请得到的空间)
用于区别小对象和大对象, 堆段又分为小对象堆段和大对象堆段.
每个堆中的堆段都是相互链接在一起的, 至少会有1个小对象堆段和1个大对象堆段 - 它们会在加载CLR时预留.

每个小对象的堆中只有一个短暂的堆段(Ephemeral Segment)用于存放短暂世代(第0世代和第1世代)的对象, 但也有可能包含第2世代的对象.
其他额外的堆段(0或1或更多个)中只会存放第2世代的对象.

每个大对象的堆中有一个或更多个堆段.

堆段中的空间会从较低的地址向较高的地址消耗, 即地址更小的对象比地址更大的对象更老. 这里也有一些例外将在下面说明.

堆段会在需要时向系统申请, 并且在不包含任何生存的对象时删除.
但是初始的堆段(加载时预留的)会一直保留.

每个堆中每次只会处理一段(而不是全部), 如在回收小对象和分配大对象时.
这样的设计提供了更好的性能, 因为大对象只会在第2世代回收时一同回收(代价相当昂贵).

堆段会按它们的申请顺序链接在一起, 链中的最后一个堆段一定是短暂的堆段(Ephemeral Segment).
回收的堆段(不包含任何生存的对象)不一定会被删除, 也可能被作为一个新的短暂的堆段, 这种机制仅在小对象的堆中实现.
每次分配大对象都会考虑整个大对象的堆, 而小对象仅仅考虑短暂的堆段.

分配预算(Allocation Budget)

分配预算是一个关联于每个世代的逻辑概念, 当世代的大小达到了指定的限制则会触发GC.
每个世代的预算值属性基本取决于该世代的对象的生存率, 如果生存率较高, 那么这个限制会调大使得下次对该世代的GC会得到一个更好的回收率.

判断需要回收哪个世代的垃圾

当GC被触发时, GC首先需要确定回收哪个世代.
除了分配预算外, 还有这些因素需要考虑:

  • 世代的碎片化程度 – 如果这个世代的碎片化程度比较高, 则收集这个世代将会得到更好的效果

  • 如果系统内存占用比较高, 并且每次可以回收到一定的内存, GC可能会更积极的去回收.
    这对于防止系统内存分页(把多出的内存数据转移到硬盘)很重要.

  • 如果短暂的堆段的空间已经用完, GC可能会更积极的去回收以防止申请一个新的堆段.

GC的工作流程

标记阶段 (Mark phase)

标记阶段的目标是寻找所有存活的对象.

多世代收集器的好处是每次只需要去看堆中最近的对象, 而不需要去看历史生成的所有对象.
当收集短暂世代(第0世代和第1世代)中的对象时, GC需要寻找这些世代中所有存活的对象,
运行引擎(EE)使用中的对象会标记为存活, 此外被其他对象(更老世代的对象)引用的对象也会标记为存活.

GC在标记更老的世代中的对象时会使用卡片(Cards),
JIT的帮助类会在赋值操作时设置卡片, 如果JIT的帮助类看到一个对象在短暂的范围中它会设置一个包含了源位置的卡片的字节.
在短暂世代的回收中, GC可以只看堆中其余的部分设置的卡片来找到它们对应的对象.
(译者注: 卡片表用于标记跨代引用,例如只扫代0的时候如果代1引用了代0的对象也要扫卡片表中代1的部分对象)

计划阶段 (Plan phase)

计划阶段会做一个比较来决定使用压缩(Compaction)还是清扫(Sweeps).
如果压缩效果更好则GC会开始实际的压缩, 否则GC会开始清扫.

重定位阶段 (Relocate phase)

如果GC决定要压缩, 那就要移动现有的对象, 指向这些对象的引用都需要被更新.
重定位阶段需要找出指向回收世代中的对象的所有引用.
相反, 标记阶段仅会参考存活的对象因此不需要考虑弱引用(Weak References).

压缩阶段 (Compact phase)

这个阶段的目标非常直接, 因为计划阶段已经计算了对象应该移动到的新地址, 压缩阶段会复制对象到这些新地址中.

清扫阶段 (Sweep phase)

清扫阶段会寻找夹在存活对象中的空余空间(死对象), 并在这些空间里创建一些自由对象.
相邻的死对象会被合并成一个自由对象.
创建的自由对象会放到 自由对象列表(freelist) 里.

代码流程

缩写: (译者注: 以下不会使用这些缩写)

  • WKS GC: 工作站GC.

  • SRV GC: 服务器GC

各个模式的举动

工作站GC - 不启用并发式GC

  1. 用户线程超过了分配预算并触发了GC.

  2. GC调用SuspendEE函数来停止所有受管理的线程(Managed Threads).

  3. GC决定需要回收哪个世代.

  4. 运行标记阶段.

  5. 运行计划阶段决定是用压缩还是用清扫.

  6. 如果决定压缩则运行重定位阶段和压缩阶段, 否则运行清扫阶段.

  7. GC调用RestartEE函数来恢复受管理的线程.

  8. 用户线程恢复运行.

工作站GC - 启用并发式GC

这里说明了后台GC如何运作.

  1. 用户线程超过了分配预算并触发了GC.

  2. GC调用SuspendEE函数来停止所有受管理的线程(Managed Threads).

  3. GC决定是否使用后台GC.

  4. 如果使用后台GC, 则后台GC线程会被唤醒并进行回收工作. 后台GC线程会调用RestartEE唤醒受管理的线程.

  5. 受管理的线程继续运行, 同时后台GC线程也继续它的工作.

  6. 用户线程可能又一次的超过了分配预算并触发了一个短暂的GC(我们称为前台GC), 流程和"工作站GC - 不启用并发式GC"一样.

  7. 后台GC线程再次调用SuspendEE函数, 进行标记阶段(Marking), 然后调用RestartEE函数, 再进行可以并行的清扫阶段(Sweep).

  8. 后台GC已完成工作.

服务器GC - 不启用并发式GC

  1. 用户线程超过了分配预算并触发了GC.

  2. 服务器GC线程被唤醒, 并调用SuspendEE来停止所有受管理的线程(Managed Threads).

  3. 服务器GC进行回收工作(流程和工作站GC - 不启用并发式GC相同).

  4. 服务器GC线程调用RestartEE唤醒受管理的线程.

  5. 用户线程恢复运行.

服务器GC - 启用并发式GC

流程和工作站GC - 启用并发式GC一样, 除了非后台的GC也会在服务器GC线程中完成.

物理架构

这一段旨在帮助你追踪代码流程.

当用户线程用完分配单位(Allocation Quantum), 需要一个新的分配单位时会调用try_allocate_more_space函数.

当try_allocate_more_space函数需要触发GC时会调用GarbageCollectGeneration函数.

在"工作站GC - 不启用并发式GC"的模式下, GarbageCollectGeneration会在触发GC的用户线程上完成所有工作, 代码流程是:

 GarbageCollectGeneration(){SuspendEE();garbage_collect();RestartEE();}garbage_collect(){generation_to_condemn();gc1();}gc1(){mark_phase();plan_phase();}plan_phase(){     // 实际的计划阶段, 判断要用压缩还是清扫if (compact){relocate_phase();compact_phase();}     else         make_free_lists();}

在"工作站GC - 启用并发式GC"的模式(默认模式)下, 后台GC的代码流程是:

 GarbageCollectGeneration(){SuspendEE();garbage_collect();RestartEE();}garbage_collect(){generation_to_condemn();     // 判断要用后台GC, 唤醒后台GCdo_background_gc();}do_background_gc(){init_background_gc();start_c_gc (); // 等待后台GC完成工作并重启受管理的线程wait_to_proceed();}bgc_thread_function() {     while (1){         // 等待事件// 唤醒gc1();}}gc1(){background_mark_phase();background_sweep();}

资源链接

  • .NET CLR GC Implementation

  • The Garbage Collection Handbook: The Art of Automatic Memory Management

  • Garbage collection (Wikipedia)

译者后注

这份文档简单的介绍了GC的设计和工作流程, 同时也带来和很多疑问, 例如一个程序有多少个堆和什么是卡片等
我将在CoreCLR源码探索的系列文章中分析CoreCLR的GC源码来解决这些疑问
另外博客园上已经有一位大神对CoreCLR的GC源码进行了部分分析,可以查看他的博客:http://www.cnblogs.com/kmsfan/p/5514473.html 

相关文章:

  • 《代码的未来》读书笔记:内存管理与GC那点事儿

  • CoreCLR源码探索(一) Object是什么

  • CoreCLR源码探索(二) new是什么

  • CoreCLR源码探索(三) GC内存分配器的内部实现

  • .NET跨平台之旅:corehost 是如何加载 coreclr 的

  • .NET CoreCLR开发人员指南(上)

原文地址:http://www.cnblogs.com/zkweb/p/6288457.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/326286.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CoreCLR源码探索(四) GC内存收集器的内部实现 分析篇

在这篇中我将讲述GC Collector内部的实现, 这是CoreCLR中除了JIT以外最复杂部分,下面一些概念目前尚未有公开的文档和书籍讲到。 为了分析这部分我花了一个多月的时间,期间也多次向CoreCLR的开发组提问过,我有信心以下内容都是比较准确的&am…

vue开源项目

转载自 vue开源项目 一、前台UI组件库 1.Element 优点:中文文档,ui种类比较全,ui设计简洁清晰 缺点:不够有特点 2.iView 优点:和element的UI很相似,有一些多的补充,可以相互替换 缺点&am…

linux跑循环脚本占内存,Linux下实现脚本监测特定进程占用内存情况

Linux系统下,我们可以利用以下命令来获取特定进程的运行情况:cat /proc/$PID/status其中PID是具体的进程号,这个命令打印出/proc/特定进程/status文件的内容,信息比较多,包含了物理内存/虚拟内存的使用状况&#xff0c…

如何在vm虚拟机里面安装win10操作系统

首先打开虚拟机,点击创建虚拟机 然后选择典型即可! 选择稍后安装操作系统 然后选择win10 64位 . 然后在找个路径: 默认60GB即可,也可以更改大小: 最后点击完成: 接下来我们需要用U盘制作一个启动盘…

CoreCLR源码探索(五) GC内存收集器的内部实现 调试篇

在上一篇中我分析了CoreCLR中GC的内部处理, 在这一篇我将使用LLDB实际跟踪CoreCLR中GC,关于如何使用LLDB调试CoreCLR的介绍可以看: 微软官方的文档,地址我在第3篇中的介绍,地址LLDB官方的入门文档,地址 源代码 本篇…

小米路由器青春版装linux,比较费心的折腾 篇二:小米路由器青春版折腾负载均衡...

比较费心的折腾 篇二:小米路由器青春版折腾负载均衡2020-06-04 11:00:233点赞10收藏8评论创作立场声明:本文的核心目的是对于比较久远的硬件进行折腾,提及的软件可以在官网、软件包或者github找到。开篇预警本文截图较多,但是前期…

2019蓝桥杯省赛---java---C---6(旋转)

题目描述 问题描述图片旋转是对图片最简单的处理方式之一,在本题中,你需要对图片顺时 针旋转 90 度。 我们用一个 nm 的二维数组来表示一个图片,例如下面给出一个 34 的 图片的例子:1 3 5 7 9 8 7 6 3 5 9 7这个图片顺时针旋转 9…

linux container 原理,容器概念与Linux Container原理

一、容器与LxC在像KVM等众多主机虚拟化解决方案中,对每一个虚拟机实例提供的是从底层硬件开始一直到上层的环境,在硬件级进行资源划分。虚拟机的内核是运行在硬件内核之上的。由于每个虚拟实例都有自己的运行内核,所以各实例之间有非常好的隔…

微软建议Windows 10开发人员升级到Visual Studio 2017

既然Visual Studio 2017已经发布,那就意味着微软开始专注于让其成为默认开发平台。Creators Update SDK的发布(面向即将到来的Windows 10 Creators Update)就是这种转变的一个很好的例子。该SDK只有Visual Studio 2017支持。 幸运的是&#…

p2p linux 开源项目,权威开源项目(linux系统、sip、live555)

1、IT知识交流(语言工具、系统问题、开源项目交流)http://stackoverflow.com/tags2、linux系统www.kernel.orghttp://www.gnu.org/copyleft/lesser.html3、pjsuahttp://www.pjsip.org/1、freeswitch1、opensipshttp://opensips.org/pub/opensips/4、live555http://www.live555.…

在Visual Studio中使用任何C++编译器

原文发表时间: 3/07/2017原文发表地址: Use any C Compiler with Visual Studio 微软Visual Studio 2017支持几种C编译器以适应各种各样的代码库。除了很多人熟悉的微软Visual C编译器外, Visual Studio2017还支持Clang, GCC以及其他针对某些平台的编辑器。 这篇文…

2020蓝桥杯省赛---java---B---8(数字三角形)

题目描述 时间限制: 1.0s 内存限制: 512.0MB 本题总分:20 分【问题描述】上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。 对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最 大的和。路径上的每一…

ASP.NET Core MVC 源码学习:Routing 路由

前言 最近打算抽时间看一下 ASP.NET Core MVC 的源码,特此把自己学习到的内容记录下来,也算是做个笔记吧。 路由作为 MVC 的基本部分,所以在学习 MVC 的其他源码之前还是先学习一下路由系统,ASP.NET Core 的路由系统相对于以前的…

linux netfilter 过滤数据包,Netfilter-iptabes报文过滤框架(一)

什么是Netfilter/iptableNetfilter/iptables是Linux内核内置的报文过滤框架,程序可以通过该框架完成报文过滤、地址转换(NAT)以及连接跟踪等功能。Netfilter/iptables由两部分组成,一部分是Netfilter的"钩子(hook)",这些"钩子…

AI项目十九:YOLOV8实现目标追踪

若该文为原创文章,转载请注明原文出处。 主要是学习一下实现目标追踪的原理,并测试一下效果。 目的是通过YOLOV8实现人员检测,并实现人员追踪,没个人员给分配一个ID,实现追踪的效果。 也可以统计人数。在小区办公楼…

2020蓝桥杯省赛---java---B---7(单词分析)

题目描述 【问题描述】 小蓝正在学习一门神奇的语言,这门语言中的单词都是由小写英文字母组 成,有些单词很长,远远超过正常英文单词的长度。小蓝学了很长时间也记不 住一些单词,他准备不再完全记忆这些单词,而是根据单…

Dapper源码学习和源码修改(下篇)

继上篇Dapper源码学习和源码修改 讲了下自己学习Dapper的心得之后,下篇也随之而来,上篇主要讲的入参解析那下篇自然主打出参映射了。 好了,废话不多说,开始吧。 学习之前你的先学习怎么使用Dapper,这个我在上篇都提过…

2020蓝桥杯省赛---java---B---6(成绩分析)

题目描述 时间限制: 1.0s 内存限制: 512.0MB 本题总分:15 分【问题描述】 小蓝给学生们组织了一场考试,卷面总分为 100 分,每个学生的得分都是 一个 0 到 100 的整数。请计算这次考试的最高分、最低分和平均分。【输入格式】 输入的第一行包…

2020蓝桥杯省赛---java---B---5(排序)

题目描述 思路分析 01231391,而01231314105。 让下标为j的字符提到最前,正好达到100次交换,这样也满足了最小字典序,也可以验证一下。 代码实现 package TEST;public class Main {public static void main(String[] args) {St…