java的gc为什么要分代?

我认为书上说的存活周期长短以及复制算法需要的空间都不足以解释,因为如果能够很容易得到所有的gc root的话,只要不在引用链上的直接清掉就OK,我的理…
关注者
693
被浏览
172,878
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

题主对“GC roots”的概念的理解完全错误

如果题主是把“GC roots”(根引用集合)实质上理解成了“live set”(存活的对象的集合)的概念的话,这就好理解了。否则很难理解题主想说的是什么意思。

=================================================

题主说:

我的理解是jvm不可能把GC roots找全乎了,把所有的gc root全部找出来也可以但是效率太低

所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用

例如说,这些引用可能包括:

  • 所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
  • VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
  • JNI handles,包括global handles和local handles
  • (看情况)所有当前被加载的Java类
  • (看情况)Java类的引用类型静态变量
  • (看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
  • (看情况)String常量池(StringTable)里的引用

注意,是一组必须活跃的引用,不是对象。

Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。

GC roots这组引用是tracing GC的起点。要实现语义正确的tracing GC,就必须要能完整枚举出所有的GC roots,否则就可能会漏扫描应该存活的对象,导致GC错误回收了这些被漏扫的活对象。

这就像任何递归定义的关系一样,如果只定义了递推项而不定义初始项的话,关系就无法成立——无从开始;而如果初始项定义漏了内容的话,递推出去也会漏内容。

那么分代式GC对GC roots的定义有什么影响呢?

答案是:分代式GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。

具体到分两代的分代式GC来说,如果第0代叫做young gen,第1代叫做old gen,那么如果有minor GC / young GC只收集young gen里的垃圾,则young gen属于“收集部分”,而old gen属于“非收集部分”,那么从old gen指向young gen的引用就必须作为minor GC / young GC的GC roots的一部分。

继续具体到HotSpot VM里的分两代式GC来说,除了old gen到young gen的引用之外,有些带有弱引用语义的结构,例如说记录所有当前被加载的类的SystemDictionary、记录字符串常量引用的StringTable等,在young GC时必须要作为strong GC roots,而在收集整堆的full GC时则不会被看作strong GC roots。

换句话说,young GC比full GC的GC roots还要更大一些。如果不能理解这个道理,那整个讨论也就无从谈起了。

顺带放几个传送门:

=================================================

那么分代有什么好处?

对传统的、基本的GC实现来说,由于它们在GC的整个工作过程中都要“stop-the-world”,如果能想办法缩短GC一次工作的时间长度就是件重要的事情。如果说收集整个GC堆耗时太长,那不如只收集其中的一部分?

于是就有好几种不同的划分(partition)GC堆的方式来实现部分收集,而分代式GC就是这其中的一个思路。

这个思路所基于的基本假设大家都很熟悉了:weak generational hypothesis——大部分对象的生命期很短(die young),而没有die young的对象则很可能会存活很长时间(live long)。

这是对过往的很多应用行为分析之后得出的一个假设。基于这个假设,如果让新创建的对象都在young gen里创建,然后频繁收集young gen,则大部分垃圾都能在young GC中被收集掉。由于young gen的大小配置通常只占整个GC堆的较小部分,而且较高的对象死亡率(或者说较低的对象存活率)让它非常适合使用copying算法来收集,这样就不但能降低单次GC的时间长度,还可以提高GC的工作效率。

放几个传送门:

但是!有些比较先进的GC算法是增量式(incremental)的,或者部分并发(mostly-concurrent),或者干脆完全并发(fully-concurrent)的。

例如鄙司Azul Systems的Zing JVM里的C4 GC,就是一个完全并发的GC算法。它不存在“GC整个工作流程中都要把应用stop-the-world”的问题——从算法的设计上就不存在。

然而C4却也是一个分两代的分代式GC。为什么呢?

C4 GC的前身是Azul System的上一代JVM里的“Pauseless GC”算法,而Pauseless是一个完全并发但是不分代的GC。

Oracle的HotSpot VM里的G1 GC,在最初设计的时候是不分代的部分并发+增量式GC,而后来在实际投入生产的时候使用的却也是分两代的分代式GC设计。

现在Red Hat正在开发中的Shenandoah GC是一个并发GC,它目前的设计还是不分代的,但根据过往经验看,它后期渐渐发展为分代式的可能性极其高——如果这个项目能活足够久的话。

对于这些GC来说,解决stop-the-world时间太长的问题并不是选择分代的主要原因。

就Azul的Pauless到C4的发展历程来看,选择实现分代的最大好处是,GC能够应付的应用内存分配速率(allocation rate)可以得到巨大的提升。

并发GC根本上要跟应用玩追赶游戏:应用一边在分配,GC一边在收集,如果GC收集的速度能跟得上应用分配的速度,那就一切都很完美;一旦GC开始跟不上了,垃圾就会渐渐堆积起来,最终到可用空间彻底耗尽的时候,应用的分配请求就只能暂时等一等了,等GC追赶上来。

所以,对于一个并发GC来说,能够尽快回收出越多空间,就能够应付越高的应用内存分配速率,从而更好地保持GC以完美的并发模式工作。

虽然并不是所有应用中的对象生命周期都完美吻合weak generational hypothesis的假设,但这个假设在很大范围内还是适用的,因而也可以帮助并发GC改善性能。

就先写这么多…