垃圾是指运行程序中没有任何指针指向的对象,这些对象就是需要被回收的垃圾。如果不及时对这些占用内存的垃圾进行清理,这些垃圾会一直占用内存空间直到应用程序结束,被占用的空间无法被其他对象使用,甚至可能导致内存溢出。
从Java JDK早期版本至今,Java垃圾回收器有10款分别是Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1、Epsilon、Shenandoah、ZGC。
下图展示的是7款经典垃圾回收器的组合关系图
Serial回收器是最基本历史最悠久的垃圾回收器,在jdk1.3之前回收器新生代唯一选择。以下是Serial回收器工作原理图。
Serial回收器只能单线程工作,使用标记-复制算法和stop-the-world机制负责回收新生代垃圾,在回收工作时会暂停所有用户线程。Serial Old回收器也是单线程工作,使用标记-整理算法和stop-the-world机制负责回收老年代垃圾。它主要有两个用途:1.与新生代Parallel Savenge GC配合使用。2.作为老年代CMS收集器的后备方案。
在HostSpot虚拟机中,使用"-XX:+UseSerialGC"参数指定年轻代使用Serial GC,老年代使用Serial Old GC。
由于Serial GC是一个单线程回收器,因此更适合应用在硬件资源配置低,仅限单核CPU的服务器上。
ParNew是一款并行回收器,除了使用多线程执行回收内存外和Serial GC之间几乎没有任何区别,以下是ParNew回收器工作原理图。
ParNew是一款并行回收器,使用标记-复制算法和stop-the-world机制负责回收新生代垃圾,在回收工作过程中会暂停用户线程。目前除了Serial Old,只能和CMS回收器配合使用。
在HotSpot虚拟机中,使用"-XX:+UserParNewGC"参数开启使用ParNew回收器。
使用"-XX:ParallelGCThreads={value}"参数限制GC线程数量,默认开启和CPU核数相同的线程。
当CPU核数大于8时,此时会使用公式计算ParallelGCThreads值,该公式为:3 +[(5*CPU)/8]
ParNew GC是并行回收器,在年轻代回收次数频繁情况下,使用该回收器效率会更高。同时也正是因是多线程版本,在多核CPU环境下,使用改回收器能够很好充分利用硬件资源优势,快速的完成垃圾回收提升程序吞吐量。
在HotSpot虚拟机中年轻代除了ParNew回收器是基于并行回收外,还有Parallel回收器,该回收器在JDK8版本默认使用。以下是Parallel回收器工作原理图
Parallel回收器使用标记-复制算法和stop the world机制,负责回收新生代的垃圾,在回收过程中会暂停所有用户线程。
Parallel Old回收器使用标记-整理算法和stop the world机制,负责回收老年代的垃圾,在回收过程中会暂停所有用户线程。
这里有一个问题已经有了ParNew回收器,为什么还需要Parallel?
和ParNew回收器不同,Parallel Scavenge收集器目标是达到一个可控制的吞吐量,因此也被称为吞吐量优先的垃圾回收器。此外还具有自适应调节策略(动态的调整堆内存分配情况)功能也是和ParNew回收器一个重要区别。
Parallel回收器具有高吞吐量特性,可以高效的利用CPU资源,尽快完成程序运算任务,适合在服务器环境中使用。
CMS全称Concurrent-Mark-Sweep,这是回收器是Hotspot虚拟机真正意义上的并发回收器,它第一次实现了GC线程和用户线程同时工作。CMS回收器的关注点是尽可能的缩短用户线程的停顿时间。以下是CMS工作原理图。
CMS的工作原理比较复杂,主要分四个阶段分别是初始标记、并发标记、重新标记、并发清除标记。
1.初始标记阶段(Initial-Mark),这个阶段所有用户线程会因stop-the-world机制而暂停,主要任务是标记GC Root直接关联的对象,一旦标记完成后就会恢复之前暂停的用户线程继续工作。
2.并发标记阶段(Councurrent-Mark),这个阶段任务是从GC Root的直接关联对象开始遍历整个对象图,这个过程耗时比较长,但是由于不需要停顿用户线程,可以和GC一起并发工作。
3.重新标记阶段(ReMark)阶段,由于并发标记阶段中,用户线程和GC线程同时运行因此需要修正并发标记期间因用户线程运行而导致标记产生变动的那一部分。这个过程因stop-the-word机制所有用户线程都会暂停,但是暂停时间会非常短。
4.标记清除阶段(Concurrent-Sweep),此阶段清除整理标记阶段判断已死亡的对象,释放内存空间。
总的来说CMS具有并发回收,低延迟特点。因此非常适用于在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以为用户带来较好的体验。
CMS回收器具有以下三个缺点:
1.会产生内存碎片导致并发清除后,用户线程可用的空间不足,在无法分配大对象的情况不得不其他产生full GC。
2.cms回收器对cpu资源非常敏感,在并发阶段它不会导致用户线程停顿但会因为站用了一部分线程而导致应用程序变慢,总吞吐量下降。
3.cms回收器会导致浮动垃圾,可能会出现Concurrent Mode Failure失败而导致另一次Full GC的产生。
Concurrent Mode Failure是cmd专有的日志,GC线程和用户工作线程同时工作,清理出来的老年代空间不足以存放由新生代晋升到老年代的对象,从而导致老年年代垃圾回收变成了Serial Old,导致用户线程停顿时间长,应用程序变慢。
什么是浮动垃圾?
浮动垃圾是指在并发标记阶段本来可达的对象,由于线程的作用变成了不可达了即产生了新的垃圾。
在并发标记阶段可能产生两种变动,第一种本来可达的对象,变成不可达。第二种是本来不可达对象,变成可达。对于一种情况产生的浮动垃圾,相比之下还是可以容忍的,重新标记不处理第一种情况是因为需要从GC Root开始遍历这样会给重新标记带来额外的开销。
G1是一款并发回收器,设计目标是延迟可控的情况下,获得尽可能高的吞吐量。下图所示是G1的内存结构图
G1的堆内存被分割为很多相同大小的Region,这些Region物理上是不连续的,使用不同的Region来表示Eden、Survivor、Old、Humongous。
这里需要注意设置Humongous原因是由于对于堆中的大对象,默认直接会被分配到老年代,如果是一个短期存在的大对象就会对垃圾回收器造成影响。因此G1划分了Humoungous区专门用来存储大的对象,如果一个H区装不了一个大对象,G1会寻找连续的H区来存储,有时候为了找到这些连续的区域,也会导致Full GC。大多数时候H区也会被当做老年代的一部分。
首先G1有计划的避免了在整个Java堆中进行全区域的垃圾回收。G1会跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的Region,所以叫G1也就是垃圾优先Garbage First。
G1引入Remember Set 主要是为了解决跨带引用问题,避免了扫描全部的 Old Regions,提高Young GC的回收效率。在 G1 GC 的内存结构中,每个 Region 都会有一个 RSet 维护并跟踪其他 Region 对本 Region 的引用,当 Region 中对象被移动时 RSet 也会更新引用。
Card Table实际上是就是由每个Region被分成了多个Card,如果一个老年代区CardTable中有对象指向年轻代区,就将它标记为Dirty_Card。然后通过cardTable把相关引用信息记录到引用指向对象所在region(Old区)对应remembered set中。
G1 GC垃圾回收过程主要有三个环节分别是年轻代GC,老年代并发标记过程,混合回收。当年轻代的Eden区用内存用尽时,便开始年轻代垃圾回收过程,G1的年轻代是多线程并行回收,此时会暂停所有用户线程,将年轻代存活的对象移动到survivor区间或者老年区间。当内存使用达到一定值45%时,开始老年代的并发标记过程,标记完成后马上进行混合回收,在混合回收过程中会将老年区移动存活的对象到空闲的区,这些空闲区也会成为老年代的一部分。老年代G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代回收,一次只扫描回收一小部分老年代的Region就可以了,同时这个老年代Region是和年轻代一起被回收。
G1在进行YGC时只会收集Eden区和Servivor区,所有用户线程都会被暂停,具体过程如下:
当越来越多对象晋升到老年代Old region时,为了避免堆内存被耗尽,虚拟机会触发一次混合的垃圾回收即Mixed GC,该算法并不是一个Old GC除了回收整个Young Region还会回收一部分Old Region。G1可以选择那些Old Region进行回收,根据垃圾回收耗时时间进行空间,需要注意Mixed GC并不是Full GC。
在并发标记以后,老年代中内存分段的垃圾百分比被回收,部分为分段的垃圾被计算出来,默认情况下这些老年代的内存分段会被分8次回收,该参数可以通过XX:G1MixedGCCountTarget设置。
混合回收包括八分之一的老年代内存分段和新生代内存分段,回收方法和年轻代回收一样。
由于老年代中内存分段默认会被分8次回收,因此G1会优先回收垃圾多得内存分段,垃圾占比例越高就越有机会被回收,并且有一个阈值决定内存分段是否被回收,XX:G1MixedGCLiveThresholdPercent,默认是65%.。意思是垃圾占比达到65%才会被回收。
混合回收不一定进行8次,有一个参数xx:G1HeadWastePercent,默认值10%,意味着如果回收的垃圾比例占整个堆的内存少于10%,则不会进行垃圾回收。
G1 GC内存被分割为多个region,内存回收是以region为基本单位,region直接使用标记整理算法,避免了内存碎片,这种特性有利于程序长时间运行。此外G1相对于cms另个优化是,除了追求低停顿时间外,还建立可预测停顿时间模型,在明确一个m毫秒的时间内,消耗在垃圾收集器上的时间不得超过m毫秒,由于G1可以只选择部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也得到了很好的控制。
相比cms G1还不具备全方位,压倒性的优化,比如无论是在用户程序运行过程中G1在回收垃圾产生的额外执行负载占用内存会比cms高。从经验上来说,在内存应用比较小服务器上CMS的表现大概会优于G1,而G1在大内存应用上则发挥其优势,平衡点在6到8GB之间。