Java GC

2018-12-04

image.png

垃圾回收可以有效的防止内存泄漏,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。回收机制有分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。

判断有无memory leak

判断GC策略是否合适

http://www.ityouknow.com/jvm/2017/08/29/GC-garbage-collection.html

GC算法

https://www.jianshu.com/p/43c1b262d36b

  • 标记 - 清除
    可达性分析法执行,首先遍历所有 GC Roots 对象,对从 GC Roots 对象可达的对象都打上一个可达标识
    对堆内存进行遍历,通过读取这些对象的 header 信息来获取对象是否标记可达。如果未标记则表示这些对象没有引用,就可以进行回收
  • 复制算法
    将可用内存按容量大小分为大小相等的两块,每次只使用其中一块。当这一块内存使用完毕,就将存活的对象复制到另一块上,然后再把这一块所有的对象一次性清理掉
  • 标记 - 整理
    让所有存活对象都向一端移动,然后直接清理掉一端边界外的内存

CMS

image

-XX:+UseConcMarkSweepGC

  1. 绝大部分新生成的对象都放在Eden区,当Eden区将满,JVM会因申请不到内存,而触发Young GC ,进行Eden区+有对象的Survivor区(设为S0区)垃圾回收,把存活的对象用复制算法拷贝到一个空的Survivor(S1)中,此时Eden区被清空,另外一个Survivor S0也为空。下次触发Young GC回收Eden+S0,将存活对象拷贝到S1中。新生代垃圾回收简单、粗暴、高效。
  2. 若发现Survivor区满了,则将这些对象拷贝到old区或者Survivor没满但某些对象足够Old,也拷贝到Old区(每次Young GC都会使Survivor区存活对象值+1,直到阈值)。 3.Old区也会进行垃圾收集(Young GC),发生一次 Major GC 至少伴随一次Young GC,一般比Young GC慢十倍以上。
  3. JVM在Old区申请不到内存,会进行Full GC。Old区使用一般采用Concurrent-Mark–Sweep策略回收内存。

总结:Java垃圾回收器是一种“自适应的、分代的、停止—复制、标记-清扫”式的垃圾回收器。

缺点:

  1. GC过程中会出现STW(Stop-The-World),若Old区对象太多,STW耗费大量时间。
  2. CMS收集器对CPU资源很敏感。
  3. CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
  4. CMS导致内存碎片问题。

优点:

大部分时间可与用户线程并发工作
低停顿
并发收集、停顿低。

缺点:

对CPU资源非常敏感。并发标记和并发清理与用户线程一起工作,如果用户线程也是CPU敏感的,那么必然影响用户线程。
无法处理浮动垃圾(Floating Garbage)。并发标记与并发清除过程会产生浮动垃圾,如果CMS之前预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将退化使用Serial Old收集器,重新进行老年代的垃圾收集,这样停顿时间就很长了。可使用XX:CMSInitiatingOccupancyFraction参数设置触发CMS时的老年代空间比例(剩余空间就是预留空间),在JDK1.6中默认为92%。
基于“标记-清除算法”,收集结束时会有大量空间碎片产生,导致明明剩余空间充足,却无法为大对象分配足够的连续内存。可打开-XX:+UseCMSCompactAtFullCollection开关参数(默认打开)在进行Full GC之前整理内存碎片(称为“压缩”);使用-XX:CMSFullGCsBeforeCompaction参数(默认0)设置多少次不带压缩的Full CG之后才进行一次带压缩的Full GC。内存整理无法并行,还需要STW,需要适当调整内存整理的频率,在GC性能与空间利用率之间平衡。

PS:
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处 理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。在这期间用户可能创建新的对象。为了处理这部分浮动垃圾和对象,CMS在并发清理之前,需要预留出足够空间给并发清理期间的用户线程使用。一般会显示使用-XX:CMSInitiatingOccupancyFraction参数设置触发CMS时的老年代空间比例,如果老年代增长不是太快,可以适当提高比例,以减少Full GC的次数。
关于浮动垃圾和内存碎片的问题。HDFS namenode在堆内存达到100G规模时,通常设置75%触发Full GC,不开启压缩,优先考虑STW造成的延迟。

G1收集器

优点

  • 空间整合:G1收集器采用标记-压缩算法,不会产生空间碎片,在分配大对象时不会因为找不到连续空间而提前触发下一次GC。
  • 可预测停顿:能够让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

使用并发线程扫描存活对象;使用并行线程来快速拷贝对象,降低程序暂停时间

-XX:+UseG1GC

引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷

在G1中,堆被划分成 许多个连续的区域(region)。每个区域大小相等,在1M~32M之间。JVM最多支持2000个区域,可推算G1能支持的最大内存为2000*32M=62.5G。区域(region)的大小在JVM初始化的时候决定,也可以用-XX:G1HeapReginSize设置。

在G1中没有物理上的Yong(Eden/Survivor)/Old Generation,它们是逻辑的,使用一些非连续的区域(Region)组成的。

  • G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
  • G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
  • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
  • G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

分区 Region

G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

卡片 Card

在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。

堆 Heap

G1同样可以通过-Xms/-Xmx来指定堆空间大小。当发生年轻代收集或混合收集时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为9,而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC。Full GC后,堆尺寸计算结果也会调整堆空间。

新生代收集

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

image
image

被圈起的绿色部分为新生代的区域(region),经过Young GC后存活的对象被复制到一个或者多个区域空闲中,这些被填充的区域将是新的新生代;当新生代对象的年龄(逃逸过一次Young GC年龄增加1)已经达到某个阈值(ParNew默认15),被复制到老年代的区域中。

回收过程是停顿的(STW,Stop-The-Word);回收完成之后根据Young GC的统计信息调整Eden和Survivor的大小,有助于合理利用内存,提高回收效率。

回收的过程多个回收线程并发收集。

Remembered Set

概念:G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

G1虽然保留了CMS关于代的概念,但是代已经不是物理上连续区域,而是一个逻辑的概念。在标记过程中,每个区域的对象活性都被计算,在回收时候,就可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Remark阶段新算法的运用,以及收集过程中的压缩,都弥补了CMS不足。

G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

OOM

  • StackOverFlowError: 栈内存溢出,用于深度方法调用(循环递归)
  • OutOfMemoryError: Java heap space。用于变量申请的空间大于jvm的最大值
  • OutOfMemoryError: GC overhead limit exceed。GC回收的过长时会抛出
  • OutOfMemoryError,过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit 错误会发生什么情况?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果
  • OutOfMemoryError: Direct buffer memory。堆外内存溢出,主要呈现在写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
  • OutOfMemoryError: unable to create new native thread。应用创建了太多线程
  • OutOfMemoryError: Metaspace。元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存的限制。

堆快照也可以通过设置-XX:+HeapDumpOnOutOfMemoryError使jvm在发生OOM时自动产生。

你可能感兴趣的:(Java GC)