深入浅出 JVM GC(2)

时间:2023-01-14 05:34:55

深入浅出 JVM GC(2)

# 前言

深入浅出 JVM GC(1) 中,限于上篇文章的篇幅,我们留下了一个问题 : 如何回收? 这篇文章将重点讲述这个问题。

在上篇文章中,我们也列出了一些大纲,今天我们就按照那个大纲来逐个讲解。在此,我将大纲复制过来。

垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)

有哪些垃圾收集器

  1. Serial 串行收集器(只适用于堆内存256m 以下的 JVM )
  2. ParNew 并行收集器(Serial 收集器的多线程版本)
  3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
  4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
  5. G1 收集器(JDK 9 的默认 GC)

有哪些GC

  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

1. 有哪些垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)
1. 标记清除算法

GC 中最基础的算法就是标记-清除算法,所谓标记清除,就是通过可达性分析,标记哪些是垃圾对象,然后清除。之所以说是最简单的算法,是因为后面的几种算法都是基于它的。

但是这个算法有2个不足之处:1. 碎片问题,标记清除之后会导致大量内存不连续的碎片,空间碎片太多会导致分配大对象时无法找到足够的连续内存从而提前触发 Full GC (此 GC 严重影响应用性能)。2. 效率问题,标记和清除这两个过程的效率都不高。我们通过一幅图来看看标记清除的算法:

深入浅出 JVM GC(2)

可以从上图看出,回收后,出现了大量的内存不连续的内存块。

2. 复制算法

为了解决效率问题,人们发明了一种复制算法(Coping)。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的用完了,就开始垃圾回收,将有用的对象复制到另一个空闲的内存上,清空之前使用的内存块。这样使得每次都对整个半区回收,而且也不用考虑内存碎片问题,只需要移动堆顶指针,按顺序分配即可,实现简单,运行高效。

但是凡事都是有缺点的,复制算法的缺点就是内存缩小到了原来的一半,无法充分利用内存空间。

总是有取舍的。

现代的所有商业虚拟机都是采用这种算法来回收新生代。基于统计学,人们得出99% 的对象都是朝生夕死的,所以不需要留出那么大的空间保存存活的对象,也就是不要1:1 的比例来划分内存。

通常的做法是:

将内存分为一个较大的Eden(伊甸园)空间和两块较小的 Survivor(幸存区)空间,每次使用 Eden 和其中一块 Survivor ,当回收时,将 Eden 和 Survivor 还存活着的对象一次性的复制到另外一块 Survivor 空间,最后清理掉 Eden 和刚刚使用的 Survivor 空间。Hotspot 默认的比例是 8:1:1,也就是说,每次新生代可用内存空间为新生代总空间的90%,只有10%的内存会被浪费,从一定程度上解决了复制算法浪费空间的问题。

当然,98% 的对象可回收只是一般的情况下,我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时怎么办呢?肯定需要依赖其他内存(老年代)进行所谓的分配担保(Handle Promotion)。

什么是分配担保呢?

如果另外一块 Survivor 区域没有足够空间存放上一次新生代手机下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。当然,具体细节这句话无法详细说明,我们将会在之后阐述具体细节。

3. 标记整理算法

从上面我们可以看出,复制算法的效率很高,请注意,该算法只有在对象存活率较低的时候(98% 对象可被回收)才能体现出效率。而如果一次 GC 活动之后,存活对象很多,那么就需要复制大量的对象,很明显,会导致效率不高;更关键的是,还需要额外的空间进行分配担保

所以,存活对象时间很长的老年代一般不使用该算法。

根据老年代的特点,一般使用“标记-整理(Mark-Compact)”算法,标记过程仍然与 “标记清除” 算法一样,但我们知道,标记清除算法会产生大量的内存碎片,对性能影响很大,所以标记整理算法后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象像一个方向移动,然后清理掉边界之外的内存。也就是将那些原来散落的对象移动在一起,让碎片不再存在。

可以说,标记整理算法相对于标记清除算法牺牲了一些性能,但却避免了内存碎片的产生,在大部分场合,可抵消掉整理过程中产生的性能损耗。

4. 分代收集算法

上面我们提到了几个名词,新生代,老年代,这些就是分代算法中名词。分代算法最主要的就是根据对象存活周期的不同将内存分成几块,一般是把 Java 堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理” 或者 “标记整理” 算法来进行回收。

2. 有哪些垃圾收集器

上面说的这些算法都是实现垃圾收集器的基础。

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Hotspot 虚拟机所包含的所有收集器如下图:

深入浅出 JVM GC(2)

从上图中看到,一共有6种 GC 组合(忽略 G1 和 为CMS备份的 SerialOld 组合 )。

  1. Serial + Serial Old
  2. Serial + CMS
  3. ParNew + CMS
  4. ParNew + Serial Old
  5. Parallel Scavenge + Serial Old
  6. Parallel Scavenge + Parallel Old

大家看到这里,一定有个疑问,为什么需要这么多垃圾收集器?

答案是:没有任何一种垃圾收集器是完美的,没有任何一种垃圾收集器适合所有的应用情况。

每个应用都需要自己的特定垃圾收集器,因此,可以说,GC 调优是门艺术,没有放之四海皆准的 GC。需要工程师们去根据应用的特性不断调优。

这么多 GC ,限于篇幅,我们将在 深入浅出 JVM GC(3) 中慢慢解释。
这里只是列出一个大纲。

接下来我们将说说关于 GC 的一些概念,方便阅读后面的关于 GC 处理器的文章。

3. 有哪些GC

  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

关于这些 GC 的分类,R 大一个回答比较清楚:Major GC和Full GC的区别是什么?触发条件呢?

从大的方面讲,GC 只分为两种,一种是不收集整个堆,一种是收集整个堆。

Partial GC:并不收集整个GC堆的模式

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

Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

1. YGC

YGC 又称 Young GC ,minor GC ,年轻代 GC。顾名思义,该 GC 过程发生在年轻代中。从分代算法中,我们知道,JVM 为了性能考虑,通常将内存区域根据对象生命周期的不同分为年轻代和年老代。

新创建的对象基本上都存放在年轻代(除了一些大对象),因为大多数对象都是很快变成引用不可达,所以大多数对象都在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们称之为 YGC。

什么时候发生 YGC 呢?当 Eden 不够放入新创建的对象时,也就是Eden 区满了,JVM 就会清理Eden 区的空间,将存活的对象放入 to 区,如果 to 区放不下,则直接进入老年代。如果 to 区能放下,则放入 to 区,然后清理掉无用对象,第二次 YGC 时,GC 扫描 Eden 区和 to 区,将这两个区的存活对象放入到 from 区,将 to 区清空(总之一定会保证有一个 Survivor 区是干净的),同样的,如果 from 区放不下,则通过分配担保机制进入老年代。如果 YGC 后,仍放不下新对象,则也通过分配担保进入老年代。

2. Old GC

通常,我们将 Old GC 等同于 Full GC,为什么呢?我们详细解释一下。

什么时候发生 Old GC? 当老年代空间满了的时候。也就是说通常是 YGC 后有很多对象进入到老年代,而老年代无法放下这些对象,这时候就需要对老年代 GC。而通常的 Old GC 其实就是 Full GC 。

3. Full GC

也就是全 GC ,对整个堆和方法区(如果存在)进行 GC。
哪些情况会 Full GC 呢?

  1. System.gc() 方法的调用
  2. heap dump 带 GC
  3. 永久代(方法区)空间不够
  4. 当准备出发 YGC 时,发现之前 YGC 后晋升对象的大小比目前 Old 区的剩余空间大,则不会触发 YGC ,转而直接触发 Full GC。

第四条说到晋升,什么是晋升呢?YGC 后,幸存的对象会放入到 Survivor 区,如果一个对象在多次 YGC 后仍然存活,则进入老年代,这个过程叫做晋升。每次 YGC 后,这个对象的年龄加一。当然,晋升的条件比较复杂。我们后面会详细讲述。

4. Mixed GC

G1 专属GC,这里不准备讲述这个 GC。

总结

到这里,我们解释了3种垃圾回收算法,第四个不算是算法,而是一种设计。还大致讲了5种收集器,并将这个坑留在了后面的文章里,最后讲了一些 GC 术语,YGC ,Old GC ,Full GC 等。

好了,关于5种垃圾收集器的详细介绍,我们将在 深入浅出 JVM GC(3)中详细说明。

good luck!!!