JVM提供了Serial收集器,Parallel收集器和CMS(Concurrent Mark-Sweep)并发标记清除收集器,但这些收集器有3个共同的问题:
a.老年代的回收要扫描整个Old老年代空间;
b.Young空间和Old空间是独立且连续的内存块,上面的垃圾回收器必须先决定Young和Old在虚拟空间的位置;
c.STW(stop the world)时间不可控的问题。
为解决上述问题,sun提出了G1回收器,JDK9的默认垃圾回收器是G1。JDK后续还有ZGC,Shenandoah和Epsilon(JDK 11)等。
关于以上的垃圾回收器可参考我之前的文章:https://www.jianshu.com/p/e9c9f088c090
1.G1垃圾回收器
G1是面向多CPU、大容量内存的Server端的GC,它缩短了停顿时间,提高了吞吐量,进而替换掉CMS,弥补CMS的mark-sweep产生内存碎片的问题,故G1采用了并行且整体“mark+cpmpact”,局部(Region之间)copy的算法。
G1将整个heap分成大小相同的独立分区(Region),使得Young和Old不再隔离,避免了在整个Heap中进行全区域的垃圾回收。G1跟踪每个Region的垃圾回收价值的大小(即回收的空间大小和消耗时间的经验值),并在后台维护一个优先列表,每次根据允许的收集时间回收价值最大的Region(即Garbage First),这种方式使得G1能够在有限的时间内获取尽可能高的回收效率。
如果对象的引用都是在Region内部之间,那么对象回收就会很简单。但是不同Region之间对象也是可能存在相互引用的,判断一个Region对象是否被引用,是否需要扫描整个heap空间呢 ?
为解决这个问题,G1使用Remember Set的来解决的,即在每个Region中都有一个对应的Remember Set来来记录Region对象之间以及其他收集器的Young和Old之间的引用。当JVM发现有对Reference类型数据进行写操作时,会产生一个Writer Barried暂时中断写操作,检查Reference引用的对象是否在不同的Region之间,如果是,则通过CardTable将相关引用记录保存到被引用对象所属的Region的Remember Set中,当进行垃圾回收时,在GC的根节点的枚举范围中加入Remember Set,这样避免了全heap扫描,且避免了遗漏。
G1回收过程
如上图,G1的垃圾回收流程和CMS很相似,具体过程如下:
1.初始标记
该阶段仅标记GC根节点能够直接关联的对象(需要Stop-The-World,但耗时很短);
2.并发标记
从GC的根节点对heap中的对象进行可达性分析,标记出存活的对象。并发标记耗时比较长,但是可以和用户程序并发执行。
3.最终标记
最终标记是为了修正并发标记期间用户程序运行而导致标记变动的那部分标记记录。JVM将并发标记这段时间用户程序对heap中对象的修改记录保存到Remember Set Logs中,在最终标记阶段,JVM将Remember Set Logs同步到Remember Set中(需要停顿线程,但是可以并发执行)。
4.筛选回收
G1首先对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间来制定回收计划,即根据可停顿时间长短来回决定收多少Region。此时可以采用可用户程序并行方式,但是Stop-The-World可以极大提高回收效率。