G1垃圾回收器其实是JDK7的特性,在目前JDK10都已经发布的情况下,已经不是什么新特性了,而我到它现在才关注它,可见我是有多么的懒;而我终于关注它了,可见我的懒还算是有救的 :)
G1其实是Garbage First的意思,垃圾优先? 不是,是优先处理那些垃圾多的内存块的意思。在大的理念上,它还是遵循JVM的内存分代假设(其实叫假设不准确,这是从实际Java应用的内存使用观察得到的结论):
90%的对象熬不过第一次垃圾回收,而老的对象(经历了好几次垃圾回收的对象)则有98%的概率会一直活下来。
基于这个分代假设,一般的垃圾回收器把内存分成三类: Eden(E), Suvivor(S)和Old(O), 其中Eden和Survivor都属于年轻代,Old属于老年代,新对象始终分配在Eden里面,熬过一次垃圾回收的对象就被移动到Survisor区了,经过数次垃圾回收之后还活着的对象会被移到Old区。
这样分代的好处是,把一个复杂的大问题,分成两类不同的小问题,针对不同的小问题,采用更有针对性的措施(分而治之):
- 对于年轻代的对象,由于对象来的快去得快,垃圾收集会比较频繁,因此执行时间一定要短,效率要高,因此要采用执行时间短,执行时间的长短只取决于对象个数的垃圾回收算法。但是这类回收器往往会比较浪费内存,比如Copying GC,会浪费一半的内存,以空间换取了时间。
- 对于老年代的对象,由于本身对象的个数不多,垃圾收集的次数不多,因此可以采用对内存使用比较高效的算法。
跟其它垃圾回收器不一样的是:G1虽然也把内存分成了这三大类,但是在G1里面这三大类不是泾渭分明的三大块内存,G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,可以前面一个是Eden后面一个就变成Survivor了。
这么做给G1带来了很大的好处,由于把三块内存变成了几百块内存,内存块的粒度变小了,从而可以垃圾回收工作更彻底的并行化。
G1的并行收集做得特别好,我们第一次听到并行收集应该是CMS(Concurrent Mark & Sweep)垃圾回收算法, 但是CMS的并行收集也只是在收集老年代能够起效,而在回收年轻代的时候CMS是要暂停整个应用的(Stop-the-world)。而G1整个收集全程几乎都是并行的,它回收的大致过程是这样的:
- 在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用
- 然后应用继续运行,同时G1开始Concurrent Mark
- 再次停止应用,来一个Final Mark
- 最后根据Garbage First的原则,选择一些内存块进行回收。
由于它高度的并行化,因此它在应用停止时间(Stop-the-world)这个指标上比其它的GC算法都要好。
G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。
由于内存被分成了很多小块,又带来了另外好处,由于内存块比较小,进行内存压缩整理的代价都比较小,相比其它GC算法,可以有效的规避内存碎片的问题。
说了G1的这么多好处,也该说说G1的坏处了,如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。
总结
G1通过在垃圾回收领域应用并行化的策略,把几块大内存块的回收问题,变成了几百块小内存的回收问题,使得回收算法可以高度并行化,同时也因为分成很多小块,使得垃圾回收的单位变成了小块内存,而不是整代内存,使得用户可能对回收时间进行配置,垃圾回收变得可以预期了。
分而治之、化整为零这些朴素的架构思想往往是很多牛叉技术产品背后的思想根源啊。