[翻译]编写高性能 .NET 代码 第二章:垃圾回收 基本操作
基本操作
垃圾回收的算法细节还在不断完善中,性能还会有进一步的提升。下文介绍的内容在不同的.NET版本里会略有不同,但大方向是不会有变动的。
在.net进程里会管理2个类型的内存堆:托管和非托管。本地代码申请的,以及由CLR申请的都是非托管内存,使用Windows API 的 VirtualAlloc 方法进行申请。CLR里分配的托管对象则分配在托管堆里,这些对象可以被垃圾回收处理。
在托管堆里有还进一步分为小对象对和大对象堆(LOH)。每个对象类型都有自己的一段堆内存段。每段的大小根据你的配置或者硬件配置有关,对于一个大型的应用,一个内存段可到几百M。
小对象还可以进一步分为3个世代。0代和1代总是在一个内存段里,2代则可以跨越多个内存段。包含0代和1代的内存段称为暂存段。
下图是堆的图形,分别是A段和B段
A段是小对象堆,B段是大对象堆。2代和1代开始只有几个字节大小,因为他们到目前为止是空的。
在小对象堆里分配对象存在一个3世代的生命周期,CLR在小对象堆里分配小于85000字节的对象(8.5k)。0代内存通常在段内存的结尾开始分配。这就是为什么前面看到的.NET内存分配得很快。如果快速分配失败,则在0代边界范围里找一个合适的分配地址。如果也没有合适的位置,则分配器会扩大0代的在内存段里边界范围。如果扩展范围时超过了内存段的范围,则会触发垃圾回收。
对象创建后都是0代。只要对象还存在,在GC时会将它们标记到下一代。0代和1代称为临时集合。
当触发GC时,可能会同时触发压缩,在这种情况下,GC会将对象移动到另外一个内存地址里,释放当前段的内存空间。如果没有触发压缩,也仅仅只是重新划分边界。在一些没有触发过压缩的GC后,结果如下图:
虽然对象没有移动,但边界有重新划分。
压缩可以发生在任何一代的GC里,这是一个相当耗时的过程,因为它需要重新更新对象的引用关系,这需要暂停所有的线程操作。正因为代价高,压缩操作只会在必要的时候才会进行。
一旦对象到达2代,在剩下的生命周期里会一直保持。这并不意味着2代对象会一直存在,如果所有的2代对象被回收,并且整段内存里都没有对象,则这段内存可以被操作系统回收,或者作为其他内存段的附属段。这通常出现在全回收阶段。
那么对象的活着是什么概念呢?在GC时可以通过任何已知的root节点到达对象,能找到对象的引用关系,就说明这个对象还活着。root节点可以是程序里的某个静态变量,线程里正在执行的方法(指局部对象),GC句柄(被pinned的句柄)以及在finalizer queue里的对象。请注意,如果你的对象在2代,并且可以被回收,但你在做0代GC时,它也不会被回收。他们需要等到一个完整回收时才会被干掉。
如果0代对象已经填充满一个内存段,再做压缩也不能获得足够空间时,gc会重新分配一个新的内存段。新的段将用于放置新分配的1代和0代对象。而之前段的对象都将转为2代对象。所有的0代对象转为1代,1代对象同样降为2代对象(因为不需要复制所以很容易实现)。这个新的内存段看起来如下:
如果2代内存不断增加,那么它可以存放在多个内存段里。LOH也一样可以跨越多个内存段存放。但不管有多少个内存段,0代和1代对象始终在一个内存段里。了解到这些知识对后面的学习会很有帮助。大对象堆使用另外一种规则。通常来说一些字符串或者数组,大小在8500直接以上会自动分配到LOH堆里,它不会走上面的代纪模型。出于性能考虑,LOH分配的对象不会在回收的时候做压缩过程,但从.net4.5.1开始,你可以按需做压缩了。就行2代对象那样,当对象不再LOH里需要时,你还是可以回收它使用的内存空间,但稍后我们会说到,在垃圾回收里,最理想的状态是不要将对象创建到大对象堆上。
在LOH里,分配器都是使用空闲列表的方式来寻找最合适的位置来分配,在本章的后面将探索一些技术来减少在大数据堆上减少内存碎片。
垃圾回收特定代时,会顺带回收它下面的代。例如回收1代时,会把0代的也回收。如果是2代,那么就是把所有的都回收了(包括LOH里的)。如果是0代或者1代回收时,程序会暂停执行到GC过程结束。如果是回收2代,这一部分操作可以可能会在另外一个后台线程里执行,这取决于系统配置。
垃圾回收分为4个阶段:
- 暂停--所有托管线程需要在回收开始前暂停。
- 标记--从每个root节点开始,回收器会对每个对象的引用做标记
- 压缩--移动内存对象,并重新修改引用路径以便释放内存碎片。它通常发生在小对象堆上,并在系统认为需要的时候进行,你不能手动控制。在大对象堆上,压缩不会自动发生,但你可以配置垃圾回收器按需压缩他。
- 恢复--托管线程回复执行
标记阶段实际上不需要遍历堆上的每个对象,它只会处理需要回收的堆。举个栗子,做0代收集时只会考虑0代的对象,做1代收集时则会标记0代和1代的对象。在做2代或者一次完整回收时,才需要遍历每一个活着的对象,当然这个开销就比较高了。另外需要考虑的是,一个高代的对象可能是低代对象的root节点,这会导致在做遍历时,也会遍历相关的高代对象,当然这个开销会比全回收阶段时小一些。
上面描述的过程会导致以下问题。
首先,垃圾回收所消耗的时间,取决于当前还活着的对象数量,而不是已经分配出去的数量。这就意味着,如果分配了100w个对象在一个root节点上,只要下次GC前,你把它与root切断引用关系,这100w个对象对你的回收耗时不会造成太大影响。
其次,垃圾回收的频率取决于特定的一代里分配了多少内存。一旦超过内部的一个阈值,GC将回收这一代的对象。这个过程GC会根据你的程序做动态调整。如果在某代的回收卓有成效(回收了很多对象),则在这一代的回收会频繁触发,反之则减少。另外一个触发因素就是你的程序在电脑里的可用内存。如果可用内存低于某个阈值,GC也会频繁发生,用来减少堆的总体体积。
从上面的描述里,你可能觉得GC已经超过了你的控制范围。但这其实已经离真相不远了。最简单的优化方式就是,你可以通过控制内存的分配模式来达到。你在了解GC是如何工作后,你可以根据分配速率,对象的生命周期,来选择合适的配置。
[翻译]编写高性能 .NET 代码 第二章:垃圾回收 基本操作的更多相关文章
- [翻译]编写高性能 .NET 代码 第二章:垃圾回收
返回目录 第二章:垃圾回收 垃圾回收是你开发工作中要了解的最重要的事情.它是造成性能问题里最显著的原因,但只要你保持持续的关注(代码审查,监控数据)就可以很快修复这些问题.我这里说的"显著的 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 减少分配率, 最重要的规则,缩短对象的生命周期,减少对象层次的深度,减少对象之间的引用,避免钉住对象(Pinning)
减少分配率 这个几乎不用解释,减少了内存的使用量,自然就减少GC回收时的压力,同时降低了内存碎片与CPU的使用量.你可以用一些方法来达到这一目的,但它可能会与其它设计相冲突. 你需要在设计对象时仔细检 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 避免使用终结器,避免大对象,避免复制缓冲区
避免使用终结器 如果没有必要,是不需要实现一个终结器(Finalizer).终结器的代码主要是让GC回收非托管资源用.它会在GC完成标记对象为可回收后,放入一个终结器队列里,在由另外一个线程执行队列里 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 将长生命周期对象和大对象池化
将长生命周期对象和大对象池化 请记住最开始说的原则:对象要么立即回收要么一直存在.它们要么在0代被回收,要么在2代里一直存在.有些对象本质是静态的,生命周期从它们被创建开始,到程序停止才会结束.其它对 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 配置选项
配置选项 在基于"less rope to hang yourself with"思想下,.NET 框架没有给开发提供很多太多的配置选项.但在大多数情况下,GC会跟你的硬件配置,及 ...
- [翻译] 编写高性能 .NET 代码--第二章 GC -- 减少大对象堆的碎片,在某些情况下强制执行完整GC,按需压缩大对象堆,在GC前收到消息通知,使用弱引用缓存对象
减少大对象堆的碎片 如果不能完全避免大对象堆的分配,则要尽量避免碎片化. 对于LOH不小心就会有无限增长,但LOH使用的空闲列表机制可以减轻增长的影响.利用这个空闲列表,我们可以在两块分配区域中间找到 ...
- [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Performance Counters(性能计数器)
<<返回目录 Performance Counters(性能计数器) 性能计数器是监视应用程序和系统性能的最简单的方法之一.它有几十个类别数百个计数器在,包括一些.net特有的计数器.要访 ...
- [翻译]编写高性能 .NET 代码 第一章:工具介绍 -- Visual Studio
<<返回目录 Visual Studio vs虽然不是全宇宙唯一的IDE,但它是.net开发人员最常用的开发工具.它自带一个性能分析工具,你可以使用它来做开发,不同的vs版本在工具上会略有 ...
- [翻译]编写高性能 .NET 代码 第一章:性能测试与工具 -- 平均值 vs 百分比
<<返回目录 平均值 vs 百分比 在考虑要性能测试的目标值时,我们需要考虑用什么统计口径.大多数人都会首选平均值,但在大多数情况下,这个正确的,但你也应该适当的考虑百分数.但你有可用性的 ...
随机推荐
- java.lang.reflect.InvocationTargetException
java.lang.reflect.InvocationTargetException是什么情况?java.lang.reflect.InvocationTargetExceptionat sun.r ...
- 基础 - 32位操作系统最多只支持4G内存。
32位操作系统最多只支持4G内存. CPU能不能直接访问硬盘的数据呢, 不能. 只能通过把硬盘的数据先放到内存里, 然后再从内存里访问硬盘的数据.我们平时玩游戏碰上读图loading 进度条的这个过程 ...
- sudo 无效命令
mac系统中由于不小心修改了/etc/sudoers下的权限为777,故而sudo命令不能使用. 解决办法 1.重新启动mac并且按command+s进入单用户界面 2.此时默认的系统状态是只读状态, ...
- 深入理解HashMap的扩容机制
什么时候扩容: 网上总结的会有很多,但大多都总结的不够完整或者不够准确.大多数可能值说了满足我下面条件一的情况. 扩容必须满足两个条件: 1. 存放新值的时候当前已有元素的个数必须大于等于阈值 2. ...
- float是什么样式?
什么是float样式? 让标签浮动起来,总体方向往上 right,left(右浮,左浮) 联合height,width使用,分别占用y方向和x方向多少,单位px或百分比(%) 作用对象不是页面,而是作 ...
- python_18_反射
什么是反射? -- 通过输入字符串来获取和修改 类(属性+方法),用字符串来映射内存对象,用于人机交互 反射有哪几种方法? -- getattr() --获取字符串 ...
- linkin大话面向对象--封装和隐藏
软件开发追求的境界:高内聚,低耦合 高内聚:尽可能把模块的内部数据,功能实现细节隐藏在模块内部独立完成,不允许外部直接干预 低耦合:仅暴露少量的方法给外部使用 到底为什么要对一个雷或者对象实现良好的封 ...
- Ceph,TFS,FastDFS,MogileFS,MooseFS,GlusterFS 对比
系统整体对比 对比说明 /文件系统 TFS FastDFS MogileFS MooseFS GlusterFS Ceph 开发语言 C++ C Perl C C C++ 开源协议 GPL V2 GP ...
- 豹哥嵌入式讲堂:ARM知识概要杂辑(1)- 内核架构编年史
众所周知,ARM公司是一家微处理器行业的知名企业,ARM公司本身并不靠自有的设计来制造或出售CPU,而是将处理器架构授权给有兴趣的厂家.这些厂家基本涵盖了全球领先的知名半导体企业.软件和OEM厂商:T ...
- php加入环境变量
一.配置变量 右击计算机-属性-高级系统设置-环境变量-系统变量-双击path,编辑,将php的文件夹路径添加到后面,我是:D:\wamp64\bin\php\php5.6.25 二.测试 当没有加入 ...