java GC 学习(四)

"分代垃圾回收"

Posted by Jht on February 21, 2017

stop-the-world

选择哪种GC算法,stop-the-world都是不可避免的。一旦Stop-the-world发生, 除了GC所需的线程外,其他线程都将停止工作GC调优通常就是为了改善stop-the-world的时间。

基于的分代理论的垃圾回收

垃圾回收器的创建基于以下两个前提:

  • 大多数对象的很快就会变得不可达
  • 只有极少数情况会出现旧对象持有新对象的引用

这两条被称为”弱分代假设“。为了证明此假设,在HotSpot VM中物理内存空间被划分为两部分: 新生代(young generate)和老年代(old generation)。

新生代

大部分的新创建对象分配在新生代。因为大部分对象很快就会变得不可达,所以它们被分配在新生代, 然后消失不再。当对象从新生代移除时,我们称之为”minor GC“

老年代 

存活在新生代中但未变为不可达的对象会被复制到老年代。一般来说老年代的内存空间比新生代大, 所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为”major GC“(或者full GC)。

方法区

其中存储着类和接口的元信息以及interned的字符串信息。所以这一区域并不是为老年代中存活下来的对象所定义的持久区。 方法区中也会发生GC,这里的GC同样也被称为major GC

如果老年代的对象需要持有新生代对象的引用怎么办?

在老年代中设计了”索引表(card table)“,是一个512字节的数据块。不管何时老年代需要持有新生代对象的引用时, 都会记录到此表中。当新生代中需要执行GC时,通过搜索此表决定新生代的对象是否为GC的目标对象, 从而降低遍历所有老年代对象进行检查的代价。该索引表使用写栅栏(write barrier)进行管理。 wite barrier是一个允许高性能执行minor GC的设备。尽管它会引入一个数据位的开销, 却能带来总体GC时间的大幅降低。

新生代的结构

新生代又可分为三个部分:

  • 一个Eden区
  • 两个Survivor区

对象在三个区域中的存活过程如下:

  • 大多数新生对象都被分配在Eden区
  • 第一次GC过后Eden中还存活的对象被移到其中一个Survivor区
  • 再次GC过程中,Eden中还存活的对象会被移到之前已移入对象的Survivor区
  • 一旦该Survivor区域无空间可用时,还存活的对象会从当前Survivor区移到另一个空的Survivor区。而当前Survivor区就会再次置为空状态
  • 经过数次在两个Survivor区域移动后还存活的对象最后会被移动到老年代

下图是Java VisualVmVisual GC监控图,Spaces包含新生代和老年代。

jvvm

注意

两个Survivor区域在任何时候必定有一个保持空白。如果同时有数据存在于两个Survivor区或者两个区域的的使用量都是0(空闲的时候有可能), 则意味着你的系统可能出现了运行错误。

更快的内存分配

在HotSpot VM中,使用了两项技术来实现更快的内存分配:

  • 指针碰撞(bump-the-pointer)
  • TLABs(Thread-Local Allocation Buffers)

Bump-the-pointer技术会跟踪在Eden上新创建的对象。由于新对象被分配在Eden空间的最上面, 所以后续如果有新对象创建,只需要判断新创建对象的大小是否满足剩余的Eden空间。 如果新对象满足要求,则其会被分配到Eden空间,同样位于Eden的最上面。所以当有新对象创建时, 只需要判断此新对象的大小即可,因此具有更快的内存分配速度。然而,在多线程环境下, 将会有别样的状况。为了满足多个线程在Eden空间上创建对象时的线程安全,不可避免的会引入锁, 因此随着锁竞争的开销,创建对象的性能也大打折扣。在HotSpot中正是通过TLABs解决了多线程问题。 TLABs允许每个线程在Eden上有自己的小片空间,线程只能访问其自己的TLAB区域, 因此bump-the-pointer能通过TLAB在不加锁的情况下完成快速的内存分配。

老年代垃圾回收

当老年代数据满时,便会执行老年代垃圾回收。根据GC算法的不同其执行过程也会有所区别,所以当你了解了每种GC的特点后再来理解老年代的垃圾回收就会容易很多。

7种GC类型:

gc7

gcs

Serial GC

参数控制:

  • -XX:+UseSerialGC 串行收集器

Serial GC会损耗应用的性能,这种GC是为单核CPU上的桌面应用设计的。 Serial GC适用于CPU核数较少且使用的内存空间较小的场景。 只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩

在老年代,则使用了一种称之为mark-sweep-compact的算法。

  • 首先该算法需要在老年代中标记出存活着的对象
  • 然后从前到后检查堆空间中存活的对象,并保持位置不变(把不再存活的对象清理出堆空间,称为空间清理)
  • 最后,把存活的对象移到堆空间的前面部分以保持已使用的堆空间的连续性,从而把堆空间分为两部分:有对象的和无对象的(称为空间压缩)

gcs

ParNew GC

参数控制:

  • -XX:+UseParNewGC ParNew收集器
  • -XX:ParallelGCThreads 限制线程数量

ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

gcs

Parallel GC

参数控制:

  • -XX:+UseParallelGC 使用Parallel收集器

Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。 可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例; 新生代并行,老年代串行;新生代复制算法、老年代标记-压缩,Parallel GC又被称为高吞吐GC(throughput GC)

Parallel Old GC

  • 参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-压缩算法。

CMS GC

  • 参数控制:
    • -XX:+UseConcMarkSweepGC 使用CMS收集器
    • -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
    • -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
    • -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。 目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度, 希望系统停顿时间最短,以给用户带来较好的体验。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的, 它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象, 速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作, 所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

gcs

G1 GC

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉CMS收集器。 与CMS收集器相比G1收集器有以下特点:

    1. 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
    1. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时, Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

新生代收集步骤
  • 1、G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。

gcs

  • 2、下图被圈起的绿色部分为新生代的区域(region),经过Young GC后存活的对象被复制到一个或者多个区域空闲中,这些被填充的区域将是新的新生代;当新生代对象的年龄(逃逸过一次Young GC年龄增加1)已经达到某个阈值(ParNew默认15),被复制到老年代的区域中。 回收过程是停顿的(STW,Stop-The-Word);回收完成之后根据Young GC的统计信息调整Eden和Survivor的大小,有助于合理利用内存,提高回收效率。回收的过程多个回收线程并发收集。

gcs

老年代收集步骤

和CMS类似,G1收集器收集老年代对象会有短暂停顿。

  • 1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
  • 2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
  • 3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

gcs

  • 4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
  • 5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

gcs

  • 6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

gcs

常用GC组合

新生代GC 老年代GC 描述
Serial Serial Old Serial和Serial Old都是单线程进行GC,GC时暂停所有应用线程。
ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。
如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。    
ParNew Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
G1GC G1GC XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启
-XX:MaxGCPauseMillis =50 #暂停时间目标
-XX:GCPauseIntervalMillis =200 #暂停间隔目标
-XX:+G1YoungGenSize=512m #年轻代大小
-XX:SurvivorRatio=6 #幸存区比例

参考资料

理解Java垃圾回收

GC算法 垃圾收集器