Wetts's blog

Stay Hungry, Stay Foolish.

0%

Java-JVM-GC流程.md

转自:https://www.cnblogs.com/shuiyj/p/12640692.html

1

挤满新生代的最后一个对象

我们应当知道,新创建的对象一般会被分配在新生代中。常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 将新生代分成 Eden 区,以及两个 Survivor 区。

某一时刻,我们创建的对象将 Eden 区全部挤满,这个对象就是「挤满新生代的最后一个对象」。此时,Minor GC 就触发了。

正式 Minor GC 前的检查

在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如 Minor GC 之后 Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:

  1. 老年代剩余空间大于新生代中的对象大小,那就直接 Minor GC,GC 完 survivor 不够放,老年代也绝对够放
  2. 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了「老年代空间分配担保规则」,具体来说就是看 -XX:-HandlePromotionFailure 参数是否设置了(一般都会设置)

老年代空间分配担保规则是这样的。如果老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:

  1. 老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,进行 Minor GC
  2. 老年代中剩余空间大小,小于历次 Minor GC 之后剩余对象的大小,进行 Full GC,把老年代空出来再检查

Minor GC 后的处境

前面说了,开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:

  1. Minor GC 之后的对象足够放到 Survivor 区,皆大欢喜,GC 结束
  2. Minor GC 之后的对象不够放到 Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束
  3. Minor GC 之后的对象不够放到 Survivor 区,老年代也放不下,那就只能 Full GC

实在不行只能 OOM

前面都是成功 GC 的例子,还有 3 种情况,会导致 GC 失败,报 OOM:

  1. 紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM
  2. 未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM
  3. 开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是能 OOM

转自:https://www.modb.pro/db/26526

堆内存结构

我们以 Java 官方的 HotSpot JVM 为例,在描述 GC 过程前,先了解一下堆内存的结构。

2
JVM 将堆内存分为了三部分:新生代(Young Generation),老年代(Old Generation),永久代(Permanent Generation)。其中新生代又分为三部分:伊甸园区(Eden),和两个幸存区 S0 和 S1。

注:JDK1.8 之后,Java 官方的 HotSpot JVM 去掉了永久代,取而代之的是元数据区 Metaspace。Metaspace 使用的是本地内存,而不是堆内存,也就是说在默认情况下 Metaspace 的大小只与本地内存的大小有关。因此 JDK1.8 之后,就见不到 java.lang.OutOfMemoryError: PermGen space 这种由于永久代空间不足导致的内存溢出的问题了。

垃圾回收全过程

3
新创建的对象会先被分配到到 Eden 区。JVM 刚启动时,Eden 区对象数量较少,两个 Survivor 区 S0、S1 几乎是空的。

4
随着时间的推移,Eden 区的对象越来越多。当 Eden 区放不下时(占用空间达到容量阈值),新生代就会发生垃圾回收,我们称之为 Minor GC 或者 Young GC。

5
发生 GC 时,第一步会通过可达性分析算法找到可达对象。如上图,蓝色为可达对象,其他紫色为不可达对象。第二步,被标示的可达对象会被转移到 S0(此时 S0 是 From Survivor),此时存活对象年龄加 1,三个对象年龄都变为 1。第三步,清除 Eden 区所有对象。

6
GC 后各区域对象占用情况,如上图所示。

7
程序继续运行,Eden 区再次达到容量阈值时,会再次发生 GC。这时 S0(From Survivor)已经有了对象。还是同样的步骤,通过可达性分析算法找到可达对象,然后再将 Eden 和 S0 中的可达对象转移到 S1(To Survivor),各存活对象年龄加 1。最后将 Eden 和 S0 中的所有对象清除。

8
GC 后 S0 区域被清空。如上图所示。S0 和 S1 发生了互换,S1 变成了 From Survivor,S0 变成了 To Survivor。

注意,To Survivor 区永远都为空。这实际上是垃圾回收算法-复制算法在年轻代的实际应用。把年轻代分为 Eden、S0、S1 三个区域,每次垃圾回收时把可达对象复制到 S0 或 S1,然后再清除掉 Eden 和(S1 或 S0)中的所有对象。由于每次 GC 时,新生代的可达对象非常少(绝大部分对象要被回收掉),一般不会超过新生代总体空间的 10%,所以搜寻可达对象以及复制对象的成本都会非常低。而且这种复制的方式还能避免产生堆内存碎片,提高内存利用率。很多年轻代垃圾收集器都采用复制算法,如 ParNew。

9
在程序运行过程中,新生代 GC 会反复发生,长寿对象会在 S0 和 S1 之间反复交换,年龄也会越来越大,当对象达到年龄上限时,会被晋升到老年代。这个年龄上限默认是 15,可以通过参数 -XX:MaxTenuringThreshold 设置。如下图,有些年轻代对象年龄达到了上限 15,被转移到了老年代。

10
其他晋升方式。新生代对象晋升到老年代,除了根据年龄正常晋升外。为了提高 JVM 的性能,JVM 设计者还考虑了其他晋升方式。

11
大对象直接晋升。大对象会跨过年轻代直接分配到老年代。可以通过 -XX:PretenureSizeThreshold 参数设置对象大小。如果参数被设置成 5MB,超过 5MB 的大对象会直接分配到老年代。这样做的目的,是为了避免大对象在Eden区及两个 Survivor 区之间大量的内存复制,大对象的内存复制耗时比普通对象要高很多。

注意:PretenureSizeThreshold参数只对Serial和ParNew两种回收器有效。

12
动态对象年龄判定。如果在 Survivor 空间中相同年龄对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象会直接进入老年代,而不用等到 MaxTenuringThreshold 中设置的年龄上限。上图,年龄为1的对象超过了 Survivor 空间的一半,所以这几个对象会直接进入老年代。

13
实际上,上面对动态对象年龄判定的描述并不精确。上图的场景也会导致相关对象晋升到老年代。年龄为 1 的对象加上年龄为 2 的对象超过了半数,这时包括年龄为 2 的对象以及年龄更大的对象都会被晋升到老年代。所以上图中年龄为 2 和 3 的对象都会被晋升到老年代。

老年代垃圾回收。随着年轻代对象的不断晋升,老年代的对象变得越来越多,达到容量阈值后老年代也会发生垃圾回收,我们称之为 Major GC 或者 Full GC,Full GC 并不是全局 GC,它只发生在老年代。

虽然年轻代和老年代都会发生GC,但是每次GC的时间和成本却大不相同。由于老年代空间大小一般是年轻代的几倍,再加上老年代对象存活率很高,所以整个标记过程比较慢,GC 成本也非常高。我们经常说的JVM调优,主要是为了尽量减少老年代Full GC的时间和频次。

老年代垃圾回收器,很少使用复制算法,主要为了避免大量对象的内存复制带来的时间和空间上的开销,一般采用标记清除、标记整理算法,就地标记回收。例如,老年代垃圾收集器 CMS 就采用了标记清除算法。对于标记清除算法带来的内存碎片问题,CMS 提供了两个参数做碎片整理,-XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction。