JVM

JVM: 垃圾回收

张天宇 on 2020-05-23

JVM第二篇,JVM的垃圾回收。

1. 如何判断对象可以回收

引用计数法

它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

判定效率高,但两个对象相互引用会导致内存泄漏,环形数据。

可达性分析算法

通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。连在葡萄根上的就是不能回收的,掉在盘子里的果是可以被回收的。

GCRoot 为:

  • 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
  • 方法区中静态类属性引用的对象(使用static修饰的对象)
  • 方法区常量引用的对象.(使用final修饰的对象)
  • 本地方法栈中JNI(Native方法)引用的对象. (使用Native修饰的方法, 线程中用的多.)

Memory Analyzer(MAT)由Eclipse提供的java堆分析工具。

五种引用

  1. 强引用

    只有所有GC Roots对象都不通过强引用引用该对象,该对象才能被垃圾回收。

  2. 软引用

    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。

    • 可以配合引用队列来释放软引用自身。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      /*-Xmx20m -XX:+PrintGCDetails -verbose:gc*/
      public class Demo1_1 {
      public static final int _4MB = 4 * 1024 * 1024;
      public static void soft() {
      List<SoftReference<byte[]>> list = new ArrayList<>();
      for (int i = 0; i < 5; i++) {
      SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
      System.out.println(ref.get());
      list.add(ref);
      System.out.println(list.size());
      }
      System.out.println("循环结束" + list.size());
      for (SoftReference<byte[]> ref : list) {
      System.out.println(ref.get());
      }
      }
      public static void main(String[] args) {
      soft();
      }
      }
      // 第三次循环后GC一次,第四次仍然不够,触发一次新的GC,回收软引用。
      /**
      循环结束5
      null
      null
      null
      null
      [B@677327b6
      **/
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      /*-Xmx20m -XX:+PrintGCDetails -verbose:gc*/
      public class Demo1_1 {
      public static final int _4MB = 4 * 1024 * 1024;
      public static void soft() {
      List<SoftReference<byte[]>> list = new ArrayList<>();
      // 引用队列
      ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
      for (int i = 0; i < 5; i++) {
      // 关联了引用队列,当软引用所关联的byte[]被回收时,软引用自己会加入到queue中去s
      SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
      System.out.println(ref.get());
      list.add(ref);
      System.out.println(list.size());
      }
      // 从队列中获取无用的软引用对象,并移除
      Reference<? extends byte[]> poll = queue.poll();
      while (poll != null) {
      list.remove(poll);
      poll = queue.poll();
      }
      System.out.println("循环结束" + list.size());
      for (SoftReference<byte[]> ref : list) {
      System.out.println(ref.get());
      }
      }
      public static void main(String[] args) {
      soft();
      }
      }
      /**
      5
      循环结束1
      [B@677327b6
      **/
  3. 弱引用

    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。

    • 可以配合引用队列来释放弱引用自身。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      /*-Xmx20m -XX:+PrintGCDetails -verbose:gc*/
      public class Demo1_1 {
      public static final int _4MB = 4 * 1024 * 1024;

      public static void main(String[] args) {
      List<WeakReference<byte[]>> list = new ArrayList<>();
      for (int i = 0; i < 5; i++) {
      WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
      list.add(ref);
      for (WeakReference<byte[]> w : list) {
      System.out.print(w.get() + " ");
      }
      System.out.println();
      }
      System.out.println("循环结束" + list.size());
      }
      }
      /**
      [B@1b6d3586
      [B@1b6d3586 [B@4554617c
      [B@1b6d3586 [B@4554617c [B@74a14482
      [GC (Allocation Failure) [PSYoungGen: 2077K->488K(6144K)] 14365K->12968K(19968K), 0.0016495 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [B@1b6d3586 [B@4554617c [B@74a14482 [B@1540e19d
      [GC (Allocation Failure) [PSYoungGen: 4696K->504K(6144K)] 17176K->12992K(19968K), 0.0008483 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [B@1b6d3586 [B@4554617c [B@74a14482 null [B@677327b6
      循环结束5
      **/
  4. 虚引用

    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存。
  5. 终结器引用

    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象。

2. 垃圾回收算法

标记清除

首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程

  • 标记和清除过程的效率都不高
  • 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不触发另一次垃圾收集动作

标记整理

复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存

  • 没有内存碎片,但是速度比较慢。

复制

复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:

  • 每次只对一块内存进行回收,运行高效
  • 只需移动栈顶指针,按顺序分配内存即可,实现简单
  • 内存回收时不用考虑内存碎片的出现

它的缺点是:可一次性分配的最大内存缩小了一半

3. 分代垃圾回收

新生代

主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

  1. 伊甸园(Eden)
  2. 幸存区From(ServivorFrom)
  3. 幸存区To(ServivorTo)

默认比例8:1:1,可以修改。对象首先分配在伊甸园区,当新生代空间不足时,出发Minor GC,将伊甸园区和幸存区From区存活的对象使用复制算法复制到To中,然后让存活的对象年龄+1,并且交换From和To。当对象年龄达到某个年龄(默认值、最大值为15(4bit ))时,就会把它们晋升到老年代中。在新生代中进行GC时,有可能遇到另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。当老年代空间不足,先尝试Minor GC,如果还不足触发Full GC,引发Stop the world并且时间较长。

Minor GC会引发Stop the world,暂停其他用户线程,防止其他线程根据原来地址找不到对象,访问混乱。

老年代

相关虚拟机参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx或者-XX:MaxHeapSize=size
新生代大小 -Xmn或(-XX:NewSize=size+XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRadio=radio和-XX:+UserAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRadio=radio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
Full GC前MinorGC -XX:+ScavengeBeforeFullGC

一个线程内的OOM不会导致整个Java的线程结束。OOM会清空线程占用的堆内存。

4. 垃圾回收器

串行

  • 单线程垃圾回收器
  • 适合堆内存较小时候,个人电脑
1
2
3
-XX:+UserSerialGC=Serial+SerialOld
# Serial 复制算法
# SerialOld 工作在老年代

其他用户线程都将阻塞等待垃圾回收线程结束。

吞吐量优先

  • 多线程
  • 堆内存较大,多核CPU支持
  • 让单位时间内,STW时间最短
1
2
3
4
5
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:+UseAdaptiveSizePolicy # 采用自适应新生代大小调整策略
-XX:GCTimeRatio=ratio # 调整吞吐量目标(垃圾回收时间与总时间占比,1/(1+radio)),一般设置为19
-XX:MaxGCPauseMillis=ms # 最大暂停毫秒数,默认是200,与上一个参数是冲突的
-XX:ParallelGCThreads=n # 控制运行时线程数

垃圾回收器会开启多个线程,线程数量和CPU核数相关。

响应时间优先

  • 多线程
  • 堆内存较大,多核CPU支持
  • 尽可能让STW单次时间最短
1
2
3
4
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads # 一般设置为并行线程数量的1/4
-XX:CMSInitiatingOccupancyFraction=percent # 执行CMS垃圾回收的内存占比,预留空间给浮动垃圾(在垃圾回收过程中其他用户线程产生的新垃圾)
-XX:+CMSScavengeBeforeRemark # 在重新标记之前对新生代垃圾做一次回收工作,将来扫描对象就少,减轻回收压力

只有在初始标记核重新标记时候才会STW。

G1

Garbage First(Garbage one)

使用场景
  • 同时注重吞吐量和低延迟,默认暂停目标是200ms。
  • 超大堆内存,会将堆划分为多个大小相等的Region。
  • 整体上是标记+整理算法,两个区域之间是复制算法。
相关JVM参数
1
2
3
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MacGCPauseMills=time
垃圾回收阶段

三个阶段循环

  1. Young Collection

    • 会STW
  2. Young Collection+Concurrent Mark

    • 在Young GC时会进行GCRoot的初始标记

    • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定

    • 1
      -XX:InitiatingHeapOccupancyPercent=percent(默认45%)
  3. Mixed Collection、

    会对伊甸园区、幸存区、老年代进行全面回收

    • 最终标记会STW

    • 拷贝存活会STW

    • 1
      -XX:MaxGCPauseMilklis=ms
  4. FullGC

    • SerialGC

      • 新生代内存不足发生的垃圾收集 - MinorGC
      • 老年代内存不足发生的垃圾收集 - FullGC
    • ParallelGC

      • 新生代内存不足发生的垃圾收集 - Minor GC
      • 老年代内存不足发生的垃圾收集 - Full GC
    • CMS

      • 新生代内存不足发生的垃圾收集 - Minor GC
      • 老年代内存不足
    • G1

      • 新生代内存不足发生的垃圾收集 - Minor GC
      • 老年代内存不足
  5. Young Collection跨代引用

    • 卡表与Remembered Set
    • 在引用变更时通过post-write barrier+dirty card queue
    • concurrent refinement threads更新Remembered Set
  6. Remark

  • pre-write barrier+satb_mark_queue
  1. JDK 8u20字符串去重

    • 优点:节省大量内存
    • 缺点:略微多占用了CPU时间,新生代回收时间略微增加
    • -XX:+UseStringDeduplication,默认打开
    • 将所有新分配的字符串放入一个队列
    • 当新生代回收时,G1并发检查是否有字符串重复
    • 如果它们值一样,让他们引用同一个char[]
    • 注意,他和String.intern()不一样
      • String.intern()关注的是字符串对象
      • 而字符串去重关注的是char[]
      • 在JVM内部,使用了不同的字符串表
  2. JDK 8u40并发标记类卸载

    • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
    • -XX:+ClassUnloadingWithConcurrentMark,默认启用
  3. JDK 8u60回收巨型对象

    • 一个对象大于region的一半时,称之为巨型对象
    • G1不会对巨型对象进行拷贝
    • 回收时优先被考虑
    • G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
  4. JDK 9并发标记起始时间的调整

    • 并发标记必须在堆空间占满之前完成,否则退化为Full GC
    • JDK 9之前需要使用-XX:InitiatingHeapOccupanyPercent
    • JDK 9可以动态调整
      • -XX:InitiatingHeapOccupancyPercent用来设置初始值
      • 进行数据采样并动态调整
      • 总会添加一个安全的空档空间

5. 垃圾回收调优

显示所有参数

1
java -XX:PrintFlagsFinal -version | findstr "GC"

调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO

确定目标

  • 低延迟还是高吞吐量,选择合适的回收器
  • 低延迟:CMS,G1,ZGC
  • 高吞吐量:Parallel GC

最快的GC是不发生GC

查看FullGC前后的内存占用,考虑下面几个问题:

  • 数据是不是太多
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小
  • 是否存在内存泄漏

新生代调优

  1. 新生代的特点

    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor GC的时间远远低于Full GC
  2. 理想大小

    • 新生代能容纳所有【并发量*(请求相应)】的数据

    • 幸存区大到能保留当前活跃对象+需要晋升对象

    • 晋升阈值配置得当,让长时间存活的对象尽快晋升

      • -XX:MaxTenuringThreshold=threshold
      • -XX:+PrintTenuringDistribution

老年代调优

以CMS为例

  • CMS的老年代内存设置的越大越好
  • 先尝试不做调优,如果没有FullGC,那么说明老年代空间很充裕,否则先尝试调优新生代
  • 观察发生FullGC时老年代内存占用,将老年代内存预设调大1/4~1/3
    • -XX:CMSInitiatingOccupanyFraction=percent

案例

  1. Full GC和Minor GC频繁

    GC频繁说明空间紧张。如果是新生代紧张,被塞满,幸存区空间紧张,导致空间晋升阈值降低,老年代存了很多生存周期短的对象,进而触发了Full GC。先试着增大新生代内存,增大幸存区空间和晋升阈值。

  2. 请求高峰期发生Full GC,单次暂停时间特别长(CMS)

    因为业务需求需要低延迟,所以选择了CMS。查看GC日志CMS的哪个阶段较长。比较慢的一般在重新标记阶段,耗时比较长,因为CMS要扫描整个堆内存。在重新标记前先做一次新生代回收。

    1
    -XX:+CMSScavengeBeforeRemark
  3. 老年代充裕情况下,发生Full GC(CMS JDK1.7)

    1.7及以前使用永久代作为方法区,1.8使用元空间。永久代空间不足也会导致Full GC,所以扩大永久代空间。