Java GC基础知识

Posted ylyzty

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java GC基础知识相关的知识,希望对你有一定的参考价值。

1 对象存活判断

1.1 引用计数

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的

引用计数法的缺陷:

public class ReferenceCountingGC 
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
    * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否有回收过
    */
    private byte[] bigSize = new byte[2 * _1MB];
    public static void testGC() 
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    

如果使用引用计数法objAobjB除互相引用外没有任何其他引用,但是无法被回收。

1.2 可达性分析

通过一些了GC Roots的根对象作为起点集,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象和GC Roots之间没有任何引用链,则该对象不可达,需要被回收。

GC Roots 对象

  • 虚拟机栈(局部变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区在常量引用的对象
  • 本地方法栈中Native方法引用的对象
  • Java虚拟机内部的引用
  • 被同步锁持有的对象
  • 等等

Java 引用

引用类型 描述
强引用 Object obj = new Object(); 垃圾回收器永远不会回收强引用对象
软引用 SoftReference; 系统发生内存溢出异常前,会回收软引用对象
弱引用 WeakReference; 无论内存是否足够,垃圾回收时都会回收弱引用对象
虚引用 PhantomReference; 该对象设置虚引用关联,唯一目的是该对象被回收时会收到一个系统通知

1.3 并发可达性分析

可达性分析在并发环境下存在的问题

问题1: 原本消亡的对象错误标记为存活对象(可容忍)
问题2: 原本存活的对象错误标记为消亡对象(不可容忍,导致程序出错)

可达性分析过程 —— 三色标记

  • 黑色节点: 对象已经被垃圾收集器访问过,且该对象的所有引用均已扫描
  • 灰色节点: 对象已经被垃圾收集器访问过,但该对象至少还有一个引用未被扫描
  • 白色节点: 对象尚未被垃圾收集器访问,可达性分析结束后,节点仍然为白色,则标识该对象不可达

问题2出现的原因: 同时满足以下两个条件

  • 并发标记期间程序增加一条或多条黑色对象到白色对象的引用
  • 并发标记期间删除了所有灰色对象到该白色对象的直接或间接引用,所以该对象无法被垃圾收集器访问到,导致误标记为消亡对象

因为黑色对象已经不会再被访问,所以新增的引用无法生效;同时删除了所有其他灰色对象到该白色对象的直接或间接引用,所以该对象无法从其他引用链访问到,导致存活对象被错误标记为消亡对象。

解决方案

  • 增量更新: 当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。如CMS垃圾回收器。
  • 原始快照: 当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。如G1垃圾回收器

2 垃圾回收算法

2.1 标记-清除算法

主要分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象

存在的缺陷:

  1. 执行效率不稳定,随着标记对象数量的增长而降低
  2. 内存空间碎片化问题,标记-清除会产生大量不连续的内存碎片

2.2 标记-复制算法

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。

当某一块内存快用完了,就将存活的对象复制到另一块内存上,然后把原来的内存空间直接清理即可。

相较于标记-清除算法的优点:

  1. 每次针对一个半区进行清理,不会产生碎片化的内存空间

存在的缺陷:

  1. 如果内存中多数对象都是存活对象,则会产生大量的内存间复制开销
  2. 将可用内存缩小为原来的一半,空间利用率低

现代Java虚拟机大多采用标记-复制算法回收新生代,因为新生代中只有极少数对象会存活。

2.3 标记-整理算法

首先标记出所有需要回收的对象;让所有存活对象向内存空间的一侧移动,最后清理掉边界以外的内存。

存在的缺陷:

  1. 移动存活对象并更新所有引用这些对象的地方是极为负重的操作,而且这种对象移动操作必须暂停用户程序才能进行(Stop The World)

3垃圾回收器

3.1 CMS收集器

Concurrent Mark Sweep(CMS) 收集器是一种以获取最短回收停顿时间(STW)为目标的收集器。该回收器基于标记-清除算法实现。

CMS收集器工作过程

  1. 初始标记(STW): 标记 GC Roots 能直接关联到的对象,速度很快
  2. 并发标记: 从 GC Roots 直接关联对象开始遍历整个对象图,不需要停顿用户线程
  3. 重新标记(STW): 修正并发标记,增量更新
  4. 并发清除: 清理掉所有消亡对象

CMS收集器由于使用并发-清除算法回收,会产生大量的内存空间碎片,可用暂时容忍;当内存空间的碎片化程度影响到对象分配时,再采用一次标记-整理算法,以获得规整的内存空间。

3.2 G1收集器

Grabage First(G1) 收集器开创了面向局部收集的设计思路和基于Region的内存布局形式。

基于 Region 的堆内存布局

  • 把连续的Java堆划分为多个大小相等的独立区域(Region),每一个 Region 都可用根据需分配给新生代Eden空间、Suvivor空间或者老年代空间。收集器对不同空间的 Region 块采用不同的策略处理。

  • Region 中还有一类特殊的 Humongous 区域(被视为老年代的一部分),专门用于存储大对象。G1认为只要大小超过一个Region块容量一半的对象就是大对象。每个 Region 大小为 1M ~ 32M,对于超过了整个 Region 容量的超级大对象会被存储到N个连续的Humongous Region块中。

  • G1虽然仍然保留了新生代和老年代的概念,但新生代和老年代的空间和位置不再是固定的,是一系列Region的集合。

G1收集器工作过程

  • 初始标记(STW): 标记 GC Roots 能直接关联到的对象
  • 并发标记: 从 GC Roots 直接关联对象开始遍历整个对象图,不需要停顿用户线程
  • 最终标记(STW): 处理并发标记阶段结束后仍然遗留下来的STAB(原始快照)记录
  • 筛选回收: 对各个Region块的回收价值和成本进行排序,根据期望的停顿时间指定回收计划,可用自由选择任意多个Region回收,然后将被回收的Region块中存活的对象复制到空的Region中

参考文章

  1. 深入理解Java虚拟机第3版

Java GC相关知识

Java堆的分类

  • 分为两类:YoungGen和OldGen。其中,YoungGen分为三部分:eden,from survivor和to survivor,比例默认是:8:1:1
  • PermGen不属于java堆的范畴 

    需要注意的是,从java8开始,PermGen已经被取消,取而代之的是metaspace,不同点在于:PermGen包含class metadata,class static variable和interned string,但是metaspace只有class metadata,另外两个(class static variable和interned string)从java8开始被移动到java堆里面

GC的分类

针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:

  • Partial GC:并不收集整个GC堆的模式,目前主要分为下面三类:

Young GC:只收集young gen的GC 
Old GC:只收集old gen的GC。CMS的concurrent collection就是这个模式
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。Full GC进行前默认会进行一次young GC,可通过参数-XX:+ScavengeBeforeFullGC来进行配置

  • 各种GC算法及其使用方法

Young collectorOld collectorJVM option
Serial (DefNew) Serial Mark-Sweep-Compact -XX:+UseSerialGC
Parallel scavenge (PSYoungGen) Serial Mark-Sweep-Compact(PSOldGen) -XX:+UseParallelGC
Parallel scavenge (PSYoungGen) Parallel Mark-Sweep-Compact(ParOldGen) -XX:+UseParallelOldGC
Serial (DefNew) Concurrent Mark Sweep -XX:+UseConcMarkSweepGC
-XX:-UseParNewGC
Parallel (ParNew) Concurrent Mark Sweep -XX:+UseConcMarkSweepGC 
-XX:+UseParNewGC
G1算法适用于年轻代和老年代,使用-XX:+UseG1GC来启用

GC的触发条件

1. Young GC触发条件

  • Eden区满了,会进行Young GC,将Eden和from中存活的对象放入到to中。

2. Full GC触发条件

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • oldGen最大【连续空间】不足,当新生代对象转入到老年代时,如果有大对象或者大数组,那么会出现老年代空间不足的情况;
  • PermGen空间不足
  • CMS GC时出现promotion failed和concurrent mode failure 
    promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代【最大连续空间】也不足以放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
  • 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
  • 对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

CMS GC和G1 GC介绍

1. CMS GC收集过程(CMS GC是针对于老年代的GC算法)

  • CMS Initial Mark(stop-the-world):收集阶段,开始收集所有的GC Roots和直接引用到的对象【该阶段会被统计到full gc里面】;
  • CMS-concurrent-mark:这个阶段会遍历整个老年代并且标记所有存活的对象,从“初始化标记”阶段找到的GC Roots开始。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
  • CMS-concurrent-preclean:这个阶段又是一个并发阶段,和应用线程并行运行,不会中断他们。前一个阶段在并行运行的时候,一些对象的引用已经发生了变化,当这些引用发生变化的时候,JVM会标记堆的这个区域为Dirty Card(包含被标记但是改变了的对象,被认为"dirty"),这就是 Card Marking。
  • CMS-concurrent-abortable-preclean:又一个并发阶段不会停止应用程序线程。这个阶段尝试着去承担STW的Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
  • CMS Final Remark(stop-the-world):这个阶段是CMS中第二个并且是最后一个STW的阶段。该阶段的任务是完成标记整个年老代的所有的存活对象。由于之前的预处理是并发的,它可能跟不上应用程序改变的速度,这个时候,STW是非常需要的来完成这个严酷考验的阶段。【该阶段会被统计到full gc里面】;
通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候,目的是消除紧接着的连续的几个STW阶段。
  • CMS-concurrent-sweep:和应用线程同时进行,不需要STW。这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。
  • CMS-concurrent-reset:这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

2. G1 GC(新生代和老年代通用的收集算法,不需要其他收集算法配合)

  • 【后续将进行补充】

一些JVM参数调整的经验和规则

  1. 年轻代大小选择

  • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
  • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
  • 避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.
  1. 年老代大小选择

  • 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得: 并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。
  • 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
  • 较小堆引起的碎片问题 因为年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现"碎片",可能需要进行如下配置: -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
  • 使用CMS的好处是用尽量少的新生代,经验值是128M-256M, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间非常的短,2G的内存, 大约20-80ms的应用程序停顿时间

关于一些参数

  • -XX:MaxTenuringThreshold=n Sets the maximum tenuring threshold for use in adaptive GC sizing. The current largest value is 15. The default value is 15 for the parallel collector and is 4 for CMS.(需要注意的是,这个参数默认为15,但是对于CMS来讲,默认为4,该段文字摘自官方文档)
  • -XX:+UseCMSCompactAtFullCollection:CMS使用标记-清除法进行垃圾回收,因此不对内存随便进行整理,使用该选项可以指定对内存碎片进行整理,该选项默认是开启的
  • -XX:+ScavengeBeforeFullGC:指定进行fullGC前进行一次young GC
  • -XX:CMSInitiatingOccupancyFraction:CMS被触发时老年代使用的比例
  • -XX:MaxGCPauseMillis=50:一次GC的最大时间,单位为ms,使用parallel scanvenge算法和G1的时候才会有效
  • -XX:PretenureSizeThreshold:超过设定的大小,那么对象将会直接被分配到老年代。单位为byte,默认为0,不开启该功能。(对于PS的收集算法,该选项无效)
  • -XX:+HandlerPromotionFailure:在Minore GC前,jvm会预估老年代最大可用的连续空间是否大于新生代所有对象总空间,如果小于,那么如果打开此开关,jvm会计算老年代最大可用的连续空间是否大于【历代】年轻代晋升到老年代所有对象的平均大小,如果小于,那么会进行Minore GC,否则,进行full GC; 如果此开关没有打开,那么会直接进行full GC,(目前根据jdk源码,该选项已经无效,jvm会直接进行上述的判断)

GC常见的几个误解:

  • 除了CMS和G1外,PSYoung Gen,DefNew,PSOldGen,ParOldGen等收集算法都需要stw。
  • STW(stop-the-world)并不等于full gc,full gc指发生在年轻代和老年代的gc。
  • CMS是发生在老年代的GC算法,但是其中的两个阶段,initial marking和final remark发生在年轻代和老年代,因此其stw属于full gc的统计数据里。
  • 当CMS运行过程中,老年代空间不够,默认会使用Serial gc进行一次full gc。

参考资料

  • https://www.zhihu.com/question/41922036/answer/93079526 RednaxelaFX的回答
  • http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
  • http://ifeve.com/useful-jvm-flags-part-7-cms-collector

以上是关于Java GC基础知识的主要内容,如果未能解决你的问题,请参考以下文章

Java GC算法——日志解读与分析(GC参数基础配置分析)

Java GC算法——日志解读与分析(GC参数基础配置分析)

Java GC算法——日志解读与分析(GC参数基础配置分析)

关于GC(中):Java垃圾回收相关基础知识

一文读懂Java GC原理和调优

Java-100天知识进阶-GC算法-知识铺