您的位置:首页 > 编程语言 > Java开发

GC和垃圾回收器其二

u1000241 2019-07-30 18:20 253 查看 https://my.oschina.net/u/10002

CMS

CMS是并行标记回收器,使用标记-清楚算法进行收集。适用于对时延要求较高的在线服务,不接受长时间停顿那种。但是如果服务运行较长时间,会造成严重的内存碎片。

CMS收集周期:

  1. 初始标记(STW):从GCRoots直接可达的老年代对象,新生代引用老年代的对象,这个过程是单线程的。

  1. 并发标记:这个阶段垃圾回收线程和应用线程同时运行,所以导致有些对象会从新生代晋升到老年代,有些老年代对象引用会被改变,有些对象直接分配到老年代,这些受影响的老年代对象所在的card被标记为dirty,用于重新标记阶段扫描。

  1. 预清理:为了让之后的重新标记阶段的STW时间尽可能短,这个阶段目标是在并发标记基础上将被应用线程影响的老年代对象进行扫描。

  2. 可中断清理:这个阶段和预清理阶段要做的事情差不多,也是为了降低重新标记阶段的STW,在进入重新标记阶段之前进行一次Minor GC,或者根据Eden中对象情况动态调整是否直接进入重新标记阶段。

  3. 重新标记(STW):重新扫描堆中对象,进行可达性分析,标记对象是否存活,这个阶段扫描目标是:新生代对象+Gc Roots + 前面被标记为dirty的card对应的老年代对象。如果前面预清理阶段工作做得好,这里会少华一些时间。

STW

initialMarking阶段:该阶段单线程执行,主要做两件事情:

  • 标记GC root可达老年对象
  • 遍历新生对象,标记可达老年对象

Remarking并发重新标记阶段,因为之前的标记可能重新产生引用关系:

  • 老年代中新对象被Gc Roots引用
  • 老年代的未标记对象被新生代对象引用
  • 老年代已标记对象增加新引用指向老年代其他对象
  • 新生代对象指向老年代引用被删除

CMS算法中提供了一个参数: CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。

CMS对于Old GC会后台线程轮询(2s钟)判断是否需要触发。一般有以下几种触发条件:

  • 如果没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议带上这个参数,不然会加大问题排查的难度)
  • 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
  • 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
  • 新生代的晋升担保失败,老年代是否有足够的空间容纳全部新生代对象或者历史平均晋升到老年代的对象,如果不够则提早进行一次old gc,防止下次ygc时候晋升失败。

CMS的堆内存结构如下:

  • 新生代:eden+2个s区
  • 老年代:old区
  • 持久代:1.8之前为perm区
  • 元空间:1.8之后为metaspace

同时要求这些空间必须是地址连续的。

JVM所采用的Old区垃圾收集器为CMS,CMS会在以下几种情况下发生Full GC:

  • 大对象分配到老年代时,可用空间不足
  • perm或metaspace空间不足 (JDK 8 开始HotSpot取消了perm,将类信息存放在metaspace中)
  • 晋升失败:年轻代的存活对象,需要迁移到老年代时,老年代剩余对象不足
  • promotion failed:担保失败,,gc日志会记录信息(如:[ParNew (promotion failed): 1669947K->145784K(1887488K));
  • concurrent mode failure:执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的,gc日志会记录信息(如:(concurrent mode failure): 2902473K->1221894K(3354624K), 0.3778980 secs] )

G1

未解决CMS算法空间产生碎片的一系列问题,HotSpot提供了G1算法。G1适用于那些多CPU大内存服务中,满足高吞吐需求的同时可以满足缩短GC暂停时间的需求。

特点如下:

  • 垃圾回收线程和应用线程并发执行,和CMS一样
  • 空闲内存压缩时避免冗长的暂停时间
  • 应用需要更多可预测的GC暂停时间
  • 不希望牺牲太多的吞吐性能

G1中通过将堆划分成多个大小相等的region,每个region是逻辑连续的一段内存:

每个region被标记成E,S,O和H,每个region在运行时会充当其中一种角色,H代表了Humongous,表示Region存储是大对象,当对象大小超过region大小的一半时,直接在新的一个或多个连续的region中分配,并标记成H。这样再也不用单独堆每个代进行设置了,不用担心每个代空间内存是否够用。

Region大小可以通过-XX:G1HeapRegionSize参数指定,大小只能是2的幂次方,如果-XX:G1HeapRegionSize没有设,会在初始化时根据堆大小计算region实际大小(默认将堆内存分成2048份,得到一个合理的大小)。

每个代region数量是可以动态调整的。G1不太适合面向年轻代的收集器,因为新生代大多采用复制算法就满足了。

GC方式

G1提供了三种回收模式:

  • ygc
  • mixed gc
  • fullgc

YGC

ygc是对年轻代的gc算法,一般对象都在eden region中分配,当eden region耗尽时无法申请内存时,会触发ygc,执行ygc之后活跃对象会被copy到survivor region或者晋升到old region,空闲的region被放入空闲列表中等待被使用。

mixed gc

当越来越多对象晋升到old region时,为避免堆内存耗尽,虚拟机会触发一个mixed gc,会回收整个young region还会回收一部分old region。

mixed gc通过设置阈值-XX:InitiatingHeapOccupancyPercent来设置,当老年代占整个堆空间大小百分比达到阈值时会触发一次mixed gc。

mixed gc执行过程和cms很像:

  1. initial mark: 初始标记过程,整个过程STW,标记了从GC Root可达的对象
  2. concurrent marking: 并发标记过程,整个过程gc collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
  3. remark: 最终标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
  4. clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中

full gc

如果对象内存分配较快,mixed gc来不及回收,导致老年代被填满,会触发一次full gc,g1的full gc算法是单线程执行serial old gc会导致长时间暂停时间,需要进行不断的调优,尽量避免full gc。

对象查找

G1引入了Region(分区)的概念,每个region中有个关联的Remembered Set(RS),RS数据结构是Hash,里面的数据是卡表(Card table),RS里面的存的是region中存活对象引用本region对象的关联指针。RS作用是跟踪某个堆区内的对象引用。

当region中数据发生变化时,首先会反映到card table中的一个或多个card上,RS通过扫描card table知道region中内存使用情况,在region使用过程中如果region被填满,分配内存的线程会重新选择一个一个新的region,空闲的region被组织到一个基于链表的数据结构里面,这样可以快速找到新的region。

G1如何进行对象分配

G1年轻代由eden region和survivor region两部分组成,新建的对象(除了大对象)大部分都分在eden region中,如果分配失败,说明eden region区已满,会触发ygc,回收eden region中的垃圾对象,进行空间释放。

对象分配4个阶段:

  • 栈上分配
  • TLAB
  • 共享Eden分配
  • Humongous分配

对象分配之前会进行逃逸分析,如果该对象只会在本线程使用,就将对象分配到栈上。这样在函数调用后销毁,减少堆的压力,避免gc。 如果在栈上分配不成功,会使用TLAB来分配,TLAB为线程本地分配缓冲区,目的是使对象尽快分配出来。如果对象在一个共享的空间分配,需要采用一些同步机制管理这些空间的空闲空间指针。在edne中每个线程都有一块属于自己的空间就是TLAB。这样在分配对象时就不需要进行任何同步操作了。 当TLAB中不能进行对象分配时,会尝试在Eden区中进行分配,如果大对象则直接在Humongous区分配。

小对象

G1默认优先从TLAB进行对象空间分配,如果分配失败说明当前TLAB剩余空间不能满足需求,调用allocate_new_tlab方法重新申请一块TLAB空间,G1会从eden region空间分配,小对象分配过程采用指针碰撞进行分配。

大对象

前面介绍了对于小对象的分配,如果是大对象由于TLAB中发不下,会调用G1CollectedHeap::mem_allocate()进行分配。

判断当前对象大小是否超过region大小的一半,如果大于则认定为大对象。大对象在内存分配之前,会进行加锁操作,根据所分配的大小计算出至少需要多少个连续的region。

  • 如果只需要一个region,通过new_region()直接返回一个可用的region即可。
  • 如果需要多个region,从空闲可用的region列表中找到多个连续的region,并返回第一个region序号。
  • 如果不存在连续多个region,则会扩大堆内存,尝试再次分配。
  • 如果扩大堆内存还是不够,则可能会触发一次gc。
标签:  JDK Mark