上一篇文章我们了解JVM内存模型,由于jvm的空间是有限的,为了降低接口响应时间,防止出现了内存溢出问题,难以定位错误的原因所在,这一篇总结一下gc回收的原理。

一、那些区域需要回收
我们知道程序计数器、虚拟机栈、本地方法栈,属于线程私有。由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

而Java堆、元空间则不一样。元空间存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

二、对象分配

当创建一个对象之后,在大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域, 如果在Minor GC期间发现新生代存活对象无法放入空闲的Survivor区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下)。

-XX:PretenureSizeThreshold 对象直接在old区分配内存的阀值
年轻代中当搜集器为Serial和ParNew时,-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配,该值默认为0,意思是无论值多大都现在Eden中存放,除非Eden区不能存放该值的时候才在老年代存放。 这样做的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象一般指 需要大量连续内存的Java对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间。

对象晋升

-XX:MaxTenuringThreshold 晋升年龄的阀值

对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代。

动态年龄的判断:
-XX:TargetSurvivorRatio
目标存活率,默认为50%。VM并不总是要求对象的年龄必须达到MaxTenuringThreshold这个年龄才会晋升。年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升。假如没有设置TargetSurvivorRatio这个值,当年龄为1的值为35%,年龄2的值为25%。年龄为1和年龄为2的加起来超过50%,故年龄为2的和年龄大于2的都晋升为老年代。

三、如何判断是否可以回收

GC 原理介绍

当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: Object5、6、7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

在Java, 可作为GC Roots的对象包括:

1、虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。
2、方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
3、方法区中的常量引用的对象。
4、本地方法栈中JNI(native方法)引用的对象。

即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收,需要以下的条件:
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法且没被调用过,如果有重写且在方法内建立再建立引用,不会被回收。

四、垃圾收集算法和收集器

分代收集算法
这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代、老年代、元空间. 这样就可以根据各年代特点分别采用最适当的GC算法。

新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集。使用的搜集器为:Serial、ParNew、Parallel Scavenge 采用的算法都为复制算法。

老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。使用的搜集器为Serial Old、Parallel Old 采用的是“标记—整理”CMS收集器采用的是“标记—清理”算法

分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间。在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长。为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿。G1收集器目前是采用的分区收集算法

复制算法
GC 原理介绍

新生代中的大部分对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor(from)。当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor(to)上, 最后清理掉Eden和刚才用过的Survivor的空间。 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代。

注:
那么当发生Minor gc时,JVM会首先检查老年代最大的可用连续空间是否大于新生代所有对象的总和,如果大于,那么这次YGC是安全的,如果不大于的话,JVM就需要判断HandlePromotionFailure是否允许空间分配担保。JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次Minor gc,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多)。Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间)。HandlePromotionFailure设置不允许空间分配担保,同样会要进行一次Full gc(Full gc 非常耗时尽量避免)。

标记清除算法
GC 原理介绍

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象。这个算法有两个问题。
1、效率问题: 标记和清除过程的效率都不高;
2、空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

标记整理算法
GC 原理介绍
标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

年轻代搜集器:

年轻代-Serial 收集器
最基本、发展最久的收集器,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。但是由于新生代回收得较快,所以停顿的时间非常少,而且没有线程切换的开销,因此也简单高效。通过-XX:+UseSerialGC参数启用,通过-XX:+UseParNewGC参数启用。

年轻代-ParNew 收集器
Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行。默认开启线程数和当前cpu数量相同,可以通过
-XX:ParallelGCThreads来控制垃圾收集线程的数量。通过

年轻代-Parallel Scavenge 收集器

Parallel Scavenge收集器:该收集器是我们文章中的所有例子的默认年轻代收集器。他的关注点和其他的收集器不同,其他的关注点是尽可能的缩短Full GC的时间。而该收集器关注的是一个可控的吞吐量。吞吐量=运行代码的时间/(运行代码的时间+GC的时间),通过参数-XX:MaxGCPauseMillis设置最大GC的停顿时间和-XX:GCTimeRatio 设置吞吐量的大小。-XX:+UseParallelGC参数启用。主要适合在后台运算而不需要太多交互的任务。

老轻代搜集器:

老年代-Serial Old 收集器
Serial Old 收集器是在老年代上实现垃圾收集的,是一个单线程收集器。

老年代-Parallel Old 收集器
Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量 及 CPU资源敏感 系统内使用。

老年代-CMS收集器

CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 可以通过 -XX:+UseConcMarkSweepGC指定使用CMS收集器;整个GC过程分为以下4个步骤:

  1. 初始标记(CMS initial mark)
    仅标记一下GC Roots能直接关联到的对象,速度很快。 但需要"Stop The World"(在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互)。

  2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
    进行GC Roots Tracing的过程,刚才产生的集合中标记出存活对象。同时应用程序也在运行,并不能保证可以标记出所有的存活对象。假如有一个对象 GC 线程没有标记(由于是并发的,用户线程之前没在用),然后轮到了用户线程,用户线程引用这个之前没有标记对象,不要把这个对象GC 掉,这个时候怎么办?假如这个时候处理不了,还是 GC 了,那么程序就直接报错了,这个是不允许的,解决办法可以如下:
    三色标记法
    这个算法就是把 GC 中的对象划分成三种情况:
    白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
    灰色:正在搜索的对象
    黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
    把这三种情况通过标记颜色的方式来处理,而不是立即给清除掉。当遇到用户线程把失效的对象又置为有效,通过后续的重新标记,来处理。

  3. 重新标记(CMS remark)
    为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录, 需要"Stop The World"。如果不停止用户线程。并发线程标记变动,一直无法停止。且停顿时间比初始标记稍长,但远比并发标记短。

  4. 并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩)
    回收所有的垃圾对象。

    CMS收集器 ,整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作。初始标记和重新标记都是阻塞的,但总体可以称为并发的。主要有三个比较明显的缺点。

1、 对CPU资源消耗很大
并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。CMS的默认收集线程数量是=(CPU数量+3)/4。当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

2、无法处理浮动垃圾, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾。由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集。 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly(来设定的回收阈值(上面指定的-XX:CMSInitiatingOccupancyFraction 的值,如果不指定,JVM仅在第一次使用设定值,后续则自动调整), 通过-XX:CMSInitiatingOccupancyFraction来设置触发当老年代的使用空间超过该比例后CMS 的cms gc就会被触发(JDK 1.6之后默认92%)。 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了)。

3、 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片。内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数(默认为true), 用于在Full GC后再执行一个碎片整理过程。但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0,默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩)。

附(cms经常用到的gc调整标签):

1、 -XX:CMSInitiatingOccupancyFraction=70 和-XX:+UseCMSInitiatingOccupancyOnly

这两个设置一般配合使用,一般用于『降低CMS GC频率或者增加频率、减少GC时长』的需求,防止Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC。
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)。-XX:+UseCMSInitiatingOccupancyOnly来设定阀值如果不指定,那么只会第一次内存占用率达到70%,才会进行CMS GC频率。

2、UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction
这两个设置一般配合使用。前者目前默认就是true了,也就是关键在后者上。 XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0,默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩)把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。

分区收集-G1收集器
1、G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;

2、G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;

3、G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿。

G1收集器配置

-XX:+UseG1GC
使用G1收集器

-XX:MaxGCPauseMillis=200

就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

-XX:G1HeapRegionSize=n
设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。

-XX:G1MixedGCCountTarget=8
设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标次数。默认值是 8 次混合垃圾回收。混合回收的目标是要控制在此目标次数以内。

-XX:InitiatingHeapOccupancyPercent=45(调整G1垃圾收集运行频率)

设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。
该值设置太高,会陷入Full GC泥潭之中,因为并发阶段没有足够的时间在剩下的堆空间被填满之前完成垃圾收集。如果该值设置太小,应用程序又会以超过实际需要的节奏进行大量的后台处理。

避免使用以下参数:

避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

-XX:ParallelGCThreads=n(调整G1垃圾收集的后台线程数)

设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。

如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n(调整G1垃圾收集的后台线程数)

设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

gc回收文件配置

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=D:\dump.dump

配置内存泄漏日志

-XX:+PrintGCDetails
-Xloggc:D:\gc.log

配置gc回收日志

相关文章:

  • 2021-08-23
  • 2021-09-25
  • 2021-05-24
  • 2021-05-22
  • 2021-05-27
  • 2021-08-01
  • 2021-12-19
  • 2021-12-31
猜你喜欢
  • 2021-05-09
  • 2022-12-23
  • 2022-01-20
  • 2021-08-21
  • 2021-11-12
  • 2021-11-30
  • 2021-05-30
相关资源
相似解决方案