[转]golang Garbage Collector(GC)
Author anteoy@gmail.com | Posted 2017-03-24 21:06:09

###前言 本文主要源于网络,用于自己对golang gc的一些理解和记录。 ###golang gc 历程 转自:http://studygolang.com/articles/9509         Go 的 GC 从 1.0 发布之后,一直有人说 Go 的 GC 不行。直到 1.5 版本之后,有一个大牛主导 GC 之后,现在没有人吐槽 Go 的 GC 了。 Go 的 GC 和 Java 的 GC 不一样,Java 的 GC 是几百个参数让你去搭配,让你配出来这个东西是最适合自己的场景。但是 Go 不一样,没有什么可以做,但是你可以通过一些其他的方式优化,比如减少对象的分配。但是好消息是 Go 官方一直在改进它,在 Go 1.4 版本的时候它的 GC 在 300 毫秒的时候,但是在 1.5 版本 GC 已经优化得非常好了,压缩到了40 毫秒。从 1.6 版本的 15 到 20 毫秒升级到 1.63 版本的 5 毫秒。又从 1.6.3 升级到 1. 7 版本的 3 毫秒以内,1.8 版本是 1 毫秒以内,基本上可以做到 1 毫秒以下的 GC 级别。         360 碰到 GC 问题最严重,360 整个消息推送系统是用 Go 语言写的,消息要及时送出去,GC 存在 30 毫秒卡住了,消息发送不出去。他们现在用 Go 1.8 测试,现在 GC 已经不是他们的问题了。当然,大家可能会说这有点不可信,GC 降下来了,CPU 使用率就上去了,1.7.3 和 1.8 版本中,CPU 肯定会多利用一些,CPU 的使用率相对上升了一点,但是 GC 有很大的提升。应该说,在 1.8 版本发布之后,1.9 版本现在引入了一个理念——goroutine 级别的GC,所以 1.9 版本可能还有更大的提升。 ###1.3及早期版本 作者:达达 链接:https://www.zhihu.com/question/21615032/answer/18781477(建议阅读原文) 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

============= 2014年7月7日,补充 ============= 之前回答问题的时候Go还处在1.1版本,到了1.2和1.3,Go的GC跟踪命令和GC内部实现已经有一些变化,并且根据评论中的反馈,这边一并做补充说明。Go 1.2之后的GC跟踪环境变量已经改为GODEBUG=“gctrace=1”,具体参数说明可以参考runtime包的文档。Go 1.3对GC做了优化,回收机制也改变了,从我的实验观测来看,用做内存存储时候产生的持久性的大量对象,一样是明显拖慢GC暂停时间的,但是函数内创建的局部对象一旦没被引用,是会被立即回收的,可以用runtime.SetFinalizer()观测到这个现象,我利用这个现象在v8.go项目做了一个engine实例销毁的单元测试。这里需要提醒大家,在平时开发或学习的时候gc是透明的,好像不存在一样,gc只在影响到业务的时候才会让人想起来有这样一个东西存在。gc什么时候才会影响到业务呢?举个例子,比如业务需求是延迟不得大于100ms,当gc暂停超过100ms时,就明显影响到业务了。而这篇回答针对的是gc影响的业务时的问题排查和优化方案,以及出问题前的提前自检。请不要因为这篇帖子就误以为gc是很恐怖的。接着补充一下我对技术分享的看法,有读者反馈一些描述比较容易误导新手,这当然不是我想看到的,技术分享本是好意,如果误导了新人就不好了。为避免误会,这里说明一下,这个帖子的问题是“Go 的垃圾回收机制在实践中有哪些需要注意的地方?”,所以你正在阅读的这个答案是针对Go语言回答的,其中的一些经验和思路可以用在其他语言,但肯定是不能照搬的。另外,语言表达的东西总是不那么严谨的,不同人可能产生不同理解,特别是对感受的描述,比如“多”、“少”、“大”、“小”、“长”、“短,这种没给出具体数值的描述,不同人可能有不同的理解,所以参考价值比较低。所以,对于分享的内容中,比较模糊,比较难以界定,没给出具体数据的部分,希望能抛砖引玉,大家也来实验一下,补充更多数据。对于已经给定数据的部分,也希望大家不要看一下就过了,最好也能实验一下证明数据给的是对的,自己也才有直观感受,万一数据给错了,也才能通过众人之力修订正确。我尽量在分享时提供方法,而不是纯感受或纯数据,希望可以众人拾柴火焰高,让后来者可以有更高的一个起点,不需要重新填坑,最后整个技术社区的水平能一起提升。============= 原文 ============= 不想看长篇大论的,这里先给个结论,go的gc还不完善但也不算不靠谱,关键看怎么用,尽量不要创建大量对象,也尽量不要频繁创建对象,这个道理其实在所有带gc的编程语言也都通用。想知道如何提前预防和解决问题的,请耐心看下去。先介绍下我的情况,我们团队的项目《仙侠道》在7月15号第一次接受玩家测试,这个项目的服务端完全用Go语言开发的,游戏数据都放在内存中由go 管理。在上线测试后我对程序做了很多调优工作,最初是稳定性优先,所以先解决的是内存泄漏问题,主要靠memprof来定位问题,接着是进一步提高性能,主要靠cpuprof和自己做的一些统计信息来定位问题。调优性能的过程中我从cpuprof的结果发现发现gc的scanblock调用占用的cpu竟然有40%多,于是我开始搞各种对象重用和尽量避免不必要的对象创建,效果显著,CPU占用降到了10%多。但我还是挺不甘心的,想继续优化看看。网上找资料时看到GOGCTRACE这个环境变量可以开启gc调试信息的打印,于是我就在内网测试服开启了,每当go执行gc时就会打印一行信息,内容是gc执行时间和回收前后的对象数量变化。我惊奇的发现一次gc要20多毫秒,我 服务器请求处理时间平均才33微秒,差了一个量级别呢。于是我开始关心起gc执行时间这个数值,它到底是一个恒定值呢?还是更数据多少有关呢?我带着疑问在外网玩家测试的服务器也开启了gc追踪,结果更让我冒冷汗了,gc执行时间竟然达到300多毫秒。go的gc是固定每两分钟执行一次,每次执行都是暂停整个程序的,300多毫秒应该足以导致可感受到的响应延迟。所以缩短gc执行时间就变得非常必要。从哪里入手呢?首先,可以推断gc执行时间跟数据量是相关的,内网数据少外网数据多。其次,gc追踪信息把对象数量当成重点数据来输出,估计扫描是按对象扫描的,所以对象多扫描时间长,对象少扫描时间短。于是我便开始着手降低对象数量,一开始我尝试用cgo来解决问题,由c申请和释放内存,这部分c创建的对象就不会被gc扫描了。但是实践下来发现cgo会导致原有的内存数据操作出些诡异问题,例如一个对象明明初始化了,但还是读到非预期的数据。另外还会引起go运行时报申请内存死锁的错误,我反复读了go申请内存的代码,跟我直接用c的malloc完全都没关联,实在是很诡异。我只好暂时放弃cgo的方案,另外想了个法子。一个玩家有很多数据,如果把非活跃玩家的数据序列化成一个字节数组,就等于把多个对象压缩成了一个,这样就可以大量减少对象数量。我按这个思路用快速改了一版代码,放到外网实际测试,对象数量从几百万降至几十万,gc扫描时间降至二十几微秒。效果不错,但是要用玩家数据时要反序列化,这个消耗太大,还需要再想办法。于是我索性把内存数据都改为结构体和切片存放,之前用的是对象和单向链表,所以一条数据就会有一个对象对应,改为结构体和结构体切片,就等于把多个对象数据缩减下来。结果如预期的一样,内存多消耗了一些,但是对象数量少了一个量级。其实项目之初我就担心过这样的情况,那时候到处问人,对象多了会不会增加gc负担,导致gc时间过长,结果没得到答案。现在我填过这个坑了,可以确定的说,会。大家就不要再往这个坑跳了。如果go的gc聪明一点,把老对象和新对象区别处理,至少在我这个应用场景可以减少不必要的扫描,如果gc可以异步进行不暂停程序,我才不在乎那几百毫秒的执行时间呢。但是也不能完全怪go不完善,如果一开始我早点知道用GOGCTRACE来观测,就可以比较早点发现问题从而比较根本的解决问题。但是既然用了,项目也上了,没办法大改,只能见招拆招了。总结以下几点给打算用go开发项目或已经在用go开发项目的朋友:1、尽早的用memprof、cpuprof、GCTRACE来观察程序。2、关注请求处理时间,特别是开发新功能的时候,有助于发现设计上的问题。3、尽量避免频繁创建对象(&abc{}、new(abc{})、make()),在频繁调用的地方可以做对象重用。4、尽量不要用go管理大量对象,内存数据库可以完全用c实现好通过cgo来调用。手机回复打字好累,先写到这里,后面再来补充案例的数据。数据补充:图1,7月22日的一次cpuprof观测,采样3000多次调用,数据显示scanblock吃了43.3%的cpu。<img src=”https://pic3.zhimg.com/a30f0c02571a98849af0ea51b94e262e_b.jpg” data-rawwidth=“571” data-rawheight=“805” class=“origin_image zh-lightbox-thumb” width=“571” data-original=”图2,7月23日,对修改后的程序做cpuprof,采样1万多次调用,数据显示cpu占用降至9.8%https://pic3.zhimg.com/a30f0c02571a98849af0ea51b94e262e_r.jpg”>图2,7月23日,对修改后的程序做cpuprof,采样1万多次调用,数据显示cpu占用降至9.8%<img src=”https://pic2.zhimg.com/3dbd1a8da915a97c857170889d8b5225_b.jpg” data-rawwidth=“265” data-rawheight=“740” class=“content_image” width=“265”>数据1,外网服务器的第一次gc trace结果, 数据显示gc执行时间有400多ms,回收后对象数量1659922个:gc13(1): 308+92+1 ms , 156 -> 107 MB 3339834 -> 1659922 (12850245-11190323) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields 数据2,程序做了优化后的外网服务器gc trace结果,数据显示gc执行时间30多ms,回收后对象数量126097个:gc14(6): 16+15+1 ms, 75 -> 37 MB 1409074 -> 126097 (10335326-10209229) objects, 45(1913) handoff, 34(4823) steal, 455/283/52 yields 示例1,数据结构的重构过程:最初的数据结构类似这样// 玩家数据表的集合 type tables struct { tableA *tableA tableB *tableB tableC *tableC // …… 此处省略一大堆表 }

// 每个玩家只会有一条tableA记录 type tableA struct { fieldA int fieldB string }

// 每个玩家有多条tableB记录 type tableB struct { xxoo int ooxx int next *tableB // 指向下一条记录 }

// 每个玩家只有一条tableC记录 type tableC struct { id int value int64 } 最初的设计会导致每个玩家有一个tables对象,每个tables对象里面有一堆类似tableA和tableC这样的一对一的数据,也有一堆类似tableB这样的一对多的数据。假设有1万个玩家,每个玩家都有一条tableA和一条tableC的数据,又各有10条tableB的数据,那么将总的产生1w (tables) + 1w (tableA) + 1w (tableC) + 10w (tableB)的对象。而实际项目中,表数量会有大几十,一对多和一对一的表参半,对象数量随玩家数量的增长倍数显而易见。为什么一开始这样设计?1、因为有的表可能没有记录,用对象的形式可以用 == nil 来判断是否有记录2、一对多的表可以动态增加和删除记录,所以设计成链表3、省内存,没数据就是没数据,有数据才有对象改造后的设计:// 玩家数据表的集合 type tables struct { tableA tableA tableB []tableB tableC tableC // …… 此处省略一大堆表 }

// 每个玩家只会有一条tableA记录 type tableA struct { _is_nil bool fieldA int fieldB string }

// 每个玩家有多条tableB记录 type tableB struct { _is_nil bool xxoo int ooxx int }

// 每个玩家只有一条tableC记录 type tableC struct { _is_nil bool id int value int64 } 一对一表用结构体,一对多表用slice,每个表都加一个_is_nil的字段,用来表示当前的数据是否是有用的数据。这样修改的结果就是,一万个玩家,产生的对象总量是 1w (tables) + 1w ([]tablesB),跟之前的设计差别很明显。但是slice不会收缩,而结构体则是一开始就占了内存,所以修改后会导致内存消耗增大。参考链接:go的gc代码,scanblock等函数都在里面:http://golang.org/src/pkg/runtime/mgc0.cgo的runtime包文档有对GOGCTRACE等关键的几个环境变量做说明:http://golang.org/pkg/runtime/如何使用cpuprof和memprof,请看《Profiling Go Programs》:http://blog.golang.org/profiling-go-programs我做的一些小试验代码,优化都是基于这些试验的数据的,可以参考下:go-labs/src at master · idada/go-labs · GitHub ###1.5版本GC (建议阅读原文) 转自:http://int64.me/2016/go%E7%AC%94%E8%AE%B0-GC.html (建议阅读原文)

三色并发标记 (1.5之后使用GC方法) In a tri-color collector, every object is either white, grey, or black and we view the heap as a graph of connected objects. At the start of a GC cycle all objects are white. The GC visits all roots, which are objects directly accessible by the application such as globals and things on the stack, and colors these grey. The GC then chooses a grey object, blackens it, and then scans it for pointers to other objects. When this scan finds a pointer to a white object, it turns that object grey. This process repeats until there are no more grey objects. At this point, white objects are known to be unreachable and can be reused. 这是让标记与用户代码并发的基本保障, 基本原理: * 起初所有对象都是白色 * 扫描所有可达对象,标记为灰色,放入待处理队列 * 从队列提取灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色 * 写屏障监控对象内存修改,从新标色或是放入队列

当完成所有的扫描和标记的工作后,剩余不是白色就是黑色,分别代表要回收和活跃对象,清理操作只需要把白色对象回收内存回收就好

三色并发标记

增量

三色标记的目的,主要是用于做增量的垃圾回收。注意到,如果只有黑色和白色两种颜色,那么回收过程将不能中断,必须一次性完成,期间用户程序是不能运行的。

而使用三色标记,即使在标记过程中对象的引用关系发生了改变,例如分配内存并修改对象属性域的值,只要满足黑色对象不引用白色对象的约束条件,垃圾回收器就可以继续正常工作。于是每次并不需要将回收过程全部执行完,只是处理一部分后停下来,后续会慢慢再次触发的回收过程,实现增量回收。相当于是把垃圾回收过程打散,减少停顿时间。

写屏障 (write barrier)

如果是STW的,三色标记没有什么问题。但是如果允许用户代码跟垃圾回收同时运行,需要维护一条约束条件:

黑色对象绝对不能引用白色对象 为什么不能让黑色引用白色?因为黑色对象是活跃对象,它引用的对象是也应该属于活跃的,不应该被清理。但是,由于在三色标记算法中,黑色对象已经处理完毕,它不会被重复扫描。那么,这个对象引用的白色对象将没有机会被着色,最终会被误当作垃圾清理。 STW中,一个对象,只有它引用的对象全标记后才会标记为黑色。所以黑色对象要么引用的黑色对象,要么引用的灰色对象。不会出现黑色引用白色对象。 对于垃圾回收和用户代码并行的场景,用户代码可能会修改已经标记为黑色的对象,让它引用白色对象。看一个例子来说明这个问题:

stack -> A.ref -> B A是从栈对象直接可达,将它标记为灰色。此时B是白色对象。假设这个时候用户代码执行:

localRef = A.ref A.ref = NULL localRef是栈上面的一个黑色对象,前一行赋值语句使得它引用到B对象。后一行A.ref被置为空之后,A将不再引用到B。A是灰色但是不再引用到B了,B不会着色。localRef是黑色,处理完毕的对象,引用了B但是不会被再次处理。于是B将永远不再有机会被标记,它会被误当作垃圾清理掉!

如果实现满足这种约束条件呢?write barrier! 来自wiki的对这个术语的解释:”A write barrier in a garbage collector is a fragment of code emitted by the compiler immediately before every store operation to ensure that (e.g.) generational invariants are maintained.” 即是说,在每一处内存写操作的前面,编译器会生成的一小段代码段,来确保不要打破一些约束条件。 增量和分代,都需要维护一个write barrier。 先看分代的垃圾回收,跨越不同分代之间的引用,需要特别注意。通常情况下,大多数的交叉引用应该是由新生代对象引用老生代对象。当我们回收新生代的时候,这没有什么问题。但是当我们回收老生代的时候,如果只扫描老生代不扫描新生代,则老生代中的一些对象可能被误当作不可达对象回收掉!为了处理这种情况,可以做一个约定–如果回收老生代,那么比它年轻的新生代都要一起回收一遍。另外一种交叉引用是老生代对象引用到新生代对象,这时就需要write barrier了,所有的这种类型引用都应该记录下来,放到一个集合中,标记的时候要处理这个集合。

再看三色标记中,黑色对象不能引用白色对象。这就是一个约束条件,write barrier就是要维护这条约束。

go1.5 GC 实现过程 gc 过程

Go1.5垃圾回收的实现被划分为五个阶段:

GCoff 垃圾回收关闭状态 GCscan 扫描阶段 GCmark 标记阶段,write barrier生效 GCmarktermination 标记结束阶段,STW,分配黑色对象 GCsweep 清扫阶段 gc 过程

控制器

全程参与并发回收任务, 记录相关状态数据, 动态调整运行策略,影响并发标记工作单元的工作模式和数量, 平衡CPU资源占用。当回收结束时,参与next_gc 回收阀值设置,调整垃圾回收触发频率

过程

初始化 设置 gcprecent(GOGC) 和 next_gc 阀值

启动 在为对象分配堆内存后,mallocgo 函数会检查垃圾回收触发条件,并依照相关状态启动或参与辅助回收 垃圾回收默认以全并发,但可用环境变量或事参数禁用并发标记和并发清理,gc goroutine 一直循环,直到符合触发条件时被唤醒

标记 分俩步骤 > 扫描 :遍历相关内存区域,依照指针标记找出灰色可达对象,加入队列 。扫描函数 (gcscan_m) 启动时,用户代码和标记函数 (MarkWorker) 都在运行 > 标记 : 将灰色对象从队列中取出,将其应用对象标记为灰色,自身标记为黑色。 并发标记由多个MarkWorker goroutine 共同完成,它们在回收任务完成前绑定到 P , 然后进入休眠状态,知道被调度器唤醒

清理 清理未被标记的白色对象 ,将其内存回收

并发清理本质上是一个死循环,被唤醒后开始执行清理任务。 通过遍历所有span 对象,触发内存回收器的回收操作。任务完成后,再次休眠,等待下次任务

监控 模拟情景:服务重启,海量服务重新接入,瞬间分配大量对象,将垃圾回收触发阀值next_gc推到一个很大的值。而当服务正常后,因活跃对象远小于该阀值,造成垃圾回收迟迟无法触发,大量白色对象无法回收,造成隐形内存泄漏。同样情景也有可能由于某个算法在短期内大量使用临时变量造成 。 这个时候只有forcegc介入,才能将next_gc恢复正常, 监控服务sysmon每隔两分钟检查一次垃圾回收状态,如果超过两分钟未曾触发,就会强制执行gc

gc 过程中几种辅助结构 parfor 并行任务框架 : 关注的是任务的分配和调度,自身不具备执行能力。它将多个任务分组交给多个执行线程。然后在执行过程中重新平衡线程的任务分配,确保整个任务在最短的时间内完成 缓存队列: workbuf 无锁栈节点,本身是一个缓存容器 问题 go程序内存占用大的问题 我们模拟大量的用户请求访问后台服务,这时各服务模块能观察到明显的内存占用上升。但是当停止压测时,内存占用并未发生明显的下降。花了很长时间定位问题,使用gprof等各种方法,依然没有发现原因。最后发现原来这时正常的…主要的原因有两个,

一是go的垃圾回收有个触发阈值,这个阈值会随着每次内存使用变大而逐渐增大(如初始阈值是10MB则下一次就是20MB,再下一次就成为了40MB…),如果长时间没有触发gc go会主动触发一次(2min)。高峰时内存使用量上去后,除非持续申请内存,靠阈值触发gc已经基本不可能,而是要等最多2min主动gc开始才能触发gc。

第二个原因是go语言在向系统交还内存时只是告诉系统这些内存不需要使用了,可以回收;同时操作系统会采取“拖延症”策略,并不是立即回收,而是等到系统内存紧张时才会开始回收这样该程序又重新申请内存时就可以获得极快的分配速度。

gc时间长的问题 对于对用户响应事件有要求的后端程序,golang gc时的stop the world兼职是噩梦。根据上文的介绍,1.5版本的go再完成上述改进后应该gc性能会提升不少,但是所有的垃圾回收型语言都难免在gc时面临性能下降,对此我们对于应该尽量避免频繁创建临时堆对象(如&abc{}, new, make等)以减少垃圾收集时的扫描时间,对于需要频繁使用的临时对象考虑直接通过数组缓存进行重用;很多人采用cgo的方法自己管理内存而绕开垃圾收集,这种方法除非迫不得已个人是不推荐的(容易造成不可预知的问题),当然迫不得已的情况下还是可以考虑的,这招带来的效果还是很明显的~

goroutine泄露的问题 我们的一个服务需要处理很多长连接请求,实现时,对于每个长连接请求各开了一个读取和写入协程,全部采用endless for loop不停地处理收发数据。当连接被远端关闭后,如果不对这两个协程做处理,他们依然会一直运行,并且占用的channel也不会被释放…这里就必须十分注意,在不使用协程后一定要把他依赖的channel close并通过再协程中判断channel是否关闭以保证其退出。

如何测量GC $ go build -gcflags “-l” -o test test.go $ GODEBUG=“gctrace=1” ./test

gctrace: setting gctrace=1 causes the garbage collector to emit a single line to standard error at each collection, summarizing the amount of memory collected and the length of the pause. Setting gctrace=2 emits the same summary but also repeats each collection. 之前说了那么多,那如何测量gc的之星效率,判断它到底是否对程序的运行造成了影响呢? 第一种方式是设置godebug的环境变量,比如运行GODEBUG=gctrace=1 ./myserver,如果要想对于输出结果了解,还需要对于gc的原理进行更进一步的深入分析,这篇文章的好处在于,清晰的之处了golang的gc时间是由哪些因素决定的,因此也可以针对性的采取不同的方式提升gc的时间:

根据之前的分析也可以知道,golang中的gc是使用标记清楚法,所以gc的总时间为:

Tgc = Tseq + Tmark + Tsweep(T表示time)

Tseq表示是停止用户的 goroutine 和做一些准备活动(通常很小)需要的时间 Tmark 是堆标记时间,标记发生在所有用户 goroutine 停止时,因此可以显著地影响处理的延迟 Tsweep 是堆清除时间,清除通常与正常的程序运行同时发生,所以对延迟来说是不太关键的 之后粒度进一步细分,具体的概念还是有些不太懂:

与Tmark相关的:1 垃圾回收过程中,堆中活动对象的数量,2 带有指针的活动对象占据的内存总量 3 活动对象中的指针数量。 与Tsweep相关的:1 堆内存的总量 2 堆中的垃圾总量

如何进行gc调优(gopher大会 Danny) 硬性参数

涉及算法的问题,总是会有些参数。GOGC参数主要控制的是下一次gc开始的时候的内存使用量。

比如当前的程序使用了4M的对内存(这里说的是堆内存),即是说程序当前reachable的内存为4m,当程序占用的内存达到reachable*(1+GOGC/100)=8M的时候,gc就会被触发,开始进行相关的gc操作。

如何对GOGC的参数进行设置,要根据生产情况中的实际场景来定,比如GOGC参数提升,来减少GC的频率。

参考 go15gc https://talks.golang.org/2015/go-gc.pdf http://dave.cheney.net/tag/godebug 1.5源码分析 golang gc 探究 golang gc 基本知识 go 性能调试问题 ###常用GC算法 ####引用计数 这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个 引用计数 ,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。 优点 是实现简单,并且内存的回收很及时。 缺点 频繁更新引用计数降低了性能 循环引用问题 ####标记-清除 该方法分为两步, 标记 从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行 清除 操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如 三色标记法 )优化了这个问题。 ####分代收集 经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation) 的空间。新创建的对象存放在称为 新生代(young generation) 中(一般来说,新生代的大小会比 老年代 小很多),随着垃圾回收的重复执行,生命周期较长的对象会被 提升(promotion) 到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。