一、低延迟垃圾收集器

概念:

  衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量、延迟,三者共同构成了一个“不可能三角”。一款优秀的收集器最多可以同时达到其中的两项。在内存、吞吐量、延迟这三项指标中,目前最关键的是延迟,因为随着加算计硬件的发展、性能的提升,我们可以允许收集器多占一点内存;硬件性能的增长,处理器核心数的增加,收集器运行的对程序的影响会降低,换句话说,就是吞吐量会更高。但是对延迟则不是这样,系统内存增大,对延迟反而会带来负面影响,,比如虚拟机要回收1TB的内内存和回收1GB的内存,在时间的耗费上是不一样的,因此低延迟会成为垃圾收集器最被重视的性能指标了。

  1.1 CMS 收集器

  CMS是一种一获取最短停顿时间为目标的收集器,CMS比较符合部署在服务端上的互联网网站或者基于浏览器的B/S系统的JAVA应用。CMS收集器是基于“标记-清除”算法实现的,整个运作过程分为4部分:

  • 初始标记:仅仅只是标记一下GC Root能直接关联到的对象,速度很快,这个过程会导致STW。
  • 并发标记:根据初始标记关联到的GC Root对象开始遍历整个对象图的过程,这个过程耗时比较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  • 重新标记:重新标记是为了修正并发标记的过程当中,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录(并发的可达性分析,三色标记),这个阶段会比初始标记稍长一点,但远比并发标记时间短,这个过程会导致STW。
  • 并发清除: 清理删除标记阶段判断已经死亡的对象,由于不存在移动存活的对象,因此这个阶段也是可以与用户线程同时并发。
      CMS是一款优秀的收集器,并发收集,低停顿,但是有三个明显的缺点:
      1. CMS收集器对处理器资源非常敏感,虽然垃圾收集线程可以和用户线程并发执行,但是有个前提是,系统的处理器核心数不能太低,否则会因为占用处理器计算能力而导致应用程序变慢,减低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说如果处理器核心数在4或以上,并发回收垃圾线程只占用25%不到的处理器运算资源,并且会随着核心数增加而下降。但是当处理器核心数不足4个时,CMS对用户线程的影响就可能变得很大。
      2. CMS收集器无法处理“浮动垃圾”,有可能因为“浮动垃圾”而触发另外一次Full GC。在CMS并发标记和并发清除的同时,用户线程也在运行,这样会伴随着新的垃圾对象生成,但是这部分垃圾对象是出现在标记之后,CMS无法在当此垃圾回收就清理掉,只好等待下一次垃圾收集,这一部分垃圾就称为“浮动垃圾”,这样就要求还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像传统的垃圾收集器等到老念叨几乎完全被填满之后再进行收集,必须预留一部分空间供并发收集时的程序使用。在JDK6,默认设置下,当老年代被使用了92%的空间后,会激活Major GC,当然这个阈值可以通过-XX:CMSInitiatingOccu-pancyFraction参数来设置,但是阈值设置得太高会出现一定的风险:CMS运行期间,预留的内存空间无法满足程序分配新对象的需求,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机不得不启动后备预案:冻结程序运行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样的停顿时间就更长了。
      3. 在没有“标记-整理”之前会产生内存碎片,CMS是基于“标记-清除”来实现的一块垃圾收集器,因此垃圾收集结束后,会产生大量的空间碎片,所以往往会出现,老年代明明内存空间还很多,但遇到需要给较大的对象分配空间的时候却触发Full GC的情况。为了解决这个问题CMS提供了-XX:+UseCMSCompactFullCollection开关参数,这个参数默认是开启的,用在与当CMS不得不进行Full GC的时候开启内存碎片合并整理,由于这个过程需要移动存活的对象,因此是无法并发执行的,会产生STW。因为并不是每次Full GC都需要进行内存整理的,这样会导致停顿时间边长,因此CMS又提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS进行若干次Full GC垃圾收集不整理空间的情况之后,下一次Full GC前会先进行碎片整理(默认值为0,标识每次进入Full GC都会碎片整理)。
#### 1.2 Garbage First 收集器 ##### 1.2.1 简介   Garbage First(简称G1)开启了收集收集从分代收集面向局部收集得设计思路和基于Region的内存布局形式。   G1是一款面向服务端应用的垃圾收集器,在JDK9版本,G1宣告取代Parallel Scavenge + Paralle Old组合成为服务端下默认的垃圾收集器,而CMS则被声明为不推荐使用的收集器,如果启用CMS的话,只能和ParNew来进行搭配使用。   G1的Mixed GC模式:在G1收集器出现之前的所有收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么是整个堆(Full GC),而G1跳出了这个樊笼它可以面向对内存任何部分来组成回收集进行回收,它可以面向堆内存任何部来组成回收集(Collection Set,简称CSet),衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。 ##### 1.2.2 Region分区 **区域Region的意义:**   G1里面的Region区域的概念是不同于其他垃圾收集的一个重要区别,也是思维的方式不同,后续垃圾收集的单位都是以Region为单位的,但仍然属于分代收集器。正是由于内存被划分为小块,避免扫描整个空间堆, 要想更好的控制GC停顿时间就得有灵活扫描堆内存和回收堆内存的选择权利,而Region让这一切成为了可能 。Region的出现不必让线程每次都必须全部扫描堆文件并且一次回收全部的垃圾,让GC时间可控,并且还保留了之前垃圾收集的分代思想,可以说G1进行了取长补短,在保留之前的垃圾收集的优势后又做了一个优化升级。

Region区的特点:
  G1逻辑上虽然区分老年代和新生代,但它们一系列区域的动态集合,会发生变化。G1收集的最小单元是Region,因此可以有计划的避免在整个JAVA堆中进行全区域的垃圾收集。更具体的思路是让G1收集器去统计各个Region里面的空间大小以及回收所需要的时间,然后维护在一个优先级列表里,每次根据用户设定的收集停顿时间(-XX:MaxGCPauseMillis指定,默认200毫秒),优先处理回收价值收益最大那些Region,以保证有限的时间内,尽可能获取最高的收集效率,通过Region分区,使得可预测的gc停顿时间成为可能。默认分为: 2048个分区,每个区块大小范围为:1~32M。 Region中有类特殊的区域:
  Humongous区域,专门用来存储大对象。只要G1任务对象的大小超过Region区域大小的一半,即被认为大对象,每个Region的大小可以通过-XX:G1HeapRegionSize来设置,取值范围1MB~32MB之间,且应为2的N次幂。对于那些超过了整个Region的超级大对象,则会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看代。
  Humongous区有三个特点:

  • 每个大对象都被连续分配在老年代。对象的起始总是被分配在序列中第一个区块的起始,区块中剩下的空间将不再使用,除非该对象被回收(容易产生内存碎片)。
  • 通常来说, 大对象只有在标记清理的最后阶段或者Full gc中才会被清除,假如已经不再被引用。但是有个例外便是,对于基本类型的数组对象G1在垃圾回收的过程当中发现,引用的数量并不是很多,那么也会对其进行回收,可以通过XX:G1EagerReclaimHumongousObjects来进行关闭。
  • 大对象的分配可能会导致gc过早发生,每次一有大对象分配,G1就会检查老年代占用是否超出阈值,如果超出,会导致标记过程的发生。
  • 大对象在gc过程中是不会移动的(因为这个复制消耗是不可接受的,这也是老年代为什么不采用复制算法的原因),会产生大量的内存空间碎片,这会导致缓慢的Full GC甚至内存溢出。
1.2.3 G1垃圾收集的两个周期
- **年轻阶段(The young-only phase )**   这个阶段以普通Minor GC为始(小蓝圆圈,一个小蓝圆圈代表一次年轻代的Minot GC),并且把符合条件的存活对象放入老年代。   该阶段随着一些年轻代的对象逐步晋升到老年代而开始,当老年代的内存占用率达到一定阈值(-XX: InitiatingHeapOccupancyPercent 默认值时45)便触发三个步骤: 1. **Concurrent Start:**   这个阶段除了有一般年轻代的收集功能外,还会去并发标记老年代的存活对象(提前为下一阶段老年代回收垃圾做准备),而这个标记过程是和年轻代的垃圾回收并发经进行的, 2. **Remark:**   这个阶段会产生STW,并且完成所有标记。并且在从Remark结束到Cleanup阶段这过程,G1会并发得计算出所选的老年代区域能够释放的空间大小。 3. **Cleanup:**   判断是否需要进行空间回收,如果需要进行空间回收,则进行一次 Prepare Mixed GC。 - **空间回收阶段(The space-reclamation phase)**   老年代超过阈值,经过Ramark和Cleanup,会进入空间收集阶段,在空间收集阶段,会进行多次混合收集,除了回收年轻代Region的垃圾对象,还会移动老年代Region的对象。随着老年代移动回收的进行,当G1觉得移动更多的老年代对象不会释放出更多的内存时,空间回收阶段结束。

  G1收集器至少有一下这些关键细节问题需要处理://TODO

  1. 将JAVA堆分成若干个Region后,存在着跨Region引用的问题如何处理?
      使用记忆集(RSet)避免全堆作为GC Root扫描,每个Region上都维护着自己的记忆集,这些记忆集会记录别的Region指向自己的指针,并记录这些指针分别在那些卡页的范围之内。
  2. 在并发标记阶段如何保证收集线程和用户线程互不干扰的运行?
      通过SATB、TAMS
  3. 怎么建立可靠的停顿预测模型?
      通过衰减均值理论基础来实现。
  4. G1清除-复制,将存活的对象复制到另外的Region,发现没有空余空间的时候,一定会触发Full GC吗?
      不会,前提是发生在垃圾回收快结束的时候,也就是说,大部分的存活对象已经复制到其他的Region,并且剩余的内存空间足够程序继续去运行。在这个前提下G1会取消复制剩余得不到内存分配的对象,并且使程序能够正常的运行。

调优:

尽量减少FullGC, FullGC 一般采用标记-整理算法,效率低 STW时间长

//打印JVM初始配置的值大小
java -XX:+PrintCommandLineFlags -version