垃圾收集器
上图展示了HotSpot虚拟机所有收集器及组合(连线),它们所处区域表明是属于新生代收集器还是老年代收集器,两个收集器间有连线表明它们可以搭配使用。
Serial&Serial Old
使用-XX:UseSerialGC,新生代使用Serial GC,老年代自动使用Serial Old GC
Serial(串行)垃圾收集器是最基本、发展历史悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择。
特点:使用单个GC线程进行垃圾回收,进行垃圾收集时,必须暂停所有工作线程,直到完成。
Parallel Scavenge&Parallel Old
使用-XX:UseParallelGC / -XX:UseParallelOldGC,新生代使用Parallel Scavenge GC,老年代使用Seriallel Old GC。
特点:使用多个GC线程进行垃圾回收
JAVA8默认垃圾收集器
ParNew&CMS(ConcurrentMarkSweep)
CMS
整个执行过程分为以下4个步骤:
- 初始标记:此阶段需要“Stop The World”。仅仅是标记一下GC Roots能直接关联的对象,速度很快。
- 并发标记:与用户线程同时进行,此阶段就是进行GC Roots Tracing的过程。
- 重新标记:此阶段需要“Stop The World”。是为了修正并发标记期间用户线程继续运作导致标记发生变动的那一部分对象的标记记录,这一阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除:与用户线程同时进行,清除已被标记为垃圾的对象。
但CMS有以下三个明显的缺点:
- 总吞吐量变低:在并发阶段,由于不会导致用户线程停顿,所以垃圾回收线程会占用一定的CPU资源而导致应用程序变慢–总吞吐量降低。CMS默认启动的回收线程数是(CPU数量 + 3)/ 4,即当CPU在4个以上时垃圾收集线程会占用25%的CPU资源。
- 浮动垃圾的产生:由于CMS并发清除阶段用户线程还在运行,伴随着程序的运行自然就会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法当次回收它们,只好留待下次GC时再清理掉。此处还有个问题,在CMS运行期间预留的内存无法满足程序需要(没有足够空间用来分配对象),就会出现一次“Concurrent Mode Failure”失败,此时虚拟机将启动后备方案:临时启用Serial Old收集器来重新进行老年代的垃圾回收,这样停顿时间就很长(单线程处理&Stop the world)
- 空间碎片过多:CMS基于“标记-清除”算法实现,也就意味着收集结束会产生大量的空间碎片。
总结:CMS本质就是对标记过程进一步细化,将整个过程中耗时最长的并发标记和并发清除过程都可以用户线程同时进行,从而达到减少”Stop The World“的时间。
G1
该垃圾收集器充分利用CPU、多核环境下的硬件优势;可以并行来缩短“Stop The World”停顿时间;也可以并发让垃圾收集与用户程序同时进行;能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式吹不同时期的对象。
跨代引用问题
我们都知道Java是通过可达性分析算法来判断对象是否存活,那么假如我们进行Minor GC时会不会出现有对象被老年代引用?或者进行Old GC时会不会又有对象被年轻代引用?
答案是肯定会出现这种情况,那我们进行Minor GC的时候不光要管GC Roots,还要再去遍历老年代,这样会出现很严重的性能问题。
解决方案
对于以上问题,就产生了一个新的解决方案,我们不用去扫描整个老年代,只要在年轻代建立一个数据结构,叫做Remembered Set,它把老年代划分为N个区域,标志出哪个区域会存在跨代引用,以后进行Minor gc的时候只要把这些包含了跨代引用的内存区域加入GC Roots一起扫描就行了。
三色标记算法
标记过程:
- 刚开始所有对象都是白色,没有被访问
- 将GC Roots直接关联的对象全部置为灰色
- 遍历所有灰色对象的引用,灰色对象本身置为黑色,引用置为灰色
- 重复步骤三,直到没有灰色对象为止
- 结束时,黑色对象存活,白色对象回收
存在问题
这个过程正确执行的前提下是没有其他线程改变对象的引用关系,然而,并发标记过程中,用户线程任在运行,因此会产生漏标和错标的情况。
漏标
假设GC线程现在检查完A对象及A对象两个引用B和C并且将A对象置黑,B和C置灰。分配给GC线程的时间片用完了换上用户线程,此时用户线程执行A.B=null操作,切断了A到B的引用。
执行完A.B=null之后,B,D,E都可以被回收了,但是由于B已经变为灰色,它任会被当做存活对象,继续遍历下去B,D,E被标记为黑色,只有留到下次GC时回收。
错标(对象消失问题)
假设此时用户线程执行以下操作
B.D=null;//B到D的引用被切断
A.xx=D;//A到D的引用被建立
执行完上面语句后分配给用户线程的时间片已用完,此时GC线程运行,此时GC线程发现B只引用了E,由于A已经被标记为黑色,所以不会再遍历A,所以D会被标记为白色,最后被当作垃圾回收。
解决方案
错标只有在满足以下两个情况才会发生:
- 增量更新:CMS使用这种方式,破坏第一个条件,当黑色对象指向白色对象时进行记录,然后在重新标记阶段对这些记录再进行判断。
- 原始快照(Snapshot At The Beginning, SATB):当灰色对象要删除白色对象时,记录删除引用