JVM之GC垃圾回收器与算法深入浅出

JVM之GC浅析

Wirte by 021.

image-20210320093842591

img

img

对象头

img

堆特性

  • Eden
    • 存活对象少
  • S1
  • S2
  • Old
    • 存活对象多

对象存活晋升过程

  • 按年龄
1
2
3
4
5
对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。

Paralle Scavenge 15
CMS 6
G1 15
  • 动态年龄分配函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
//survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
total += sizes[age];//sizes数组是每个年龄段对象大小
if (total > desired_survivor_size) break;
age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
...
}

Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。

什么是垃圾?(对象引用详解)

  • 什么是强引用Strong Reference?

    1
    Object o = new Object();
    • 特点

      1
      只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。
    • 什么情况下会被回收?

      1
      2
      Object o = new Object();
      o = null;
  • 什么是软引用SoftReference?

    • 特点

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student());
      Student student = studentSoftReference.get();
      System.out.println(student);

      SoftReference<byte[]> softReference = new SoftReference<byte[]>(new byte[1024*1024*10]);
      System.out.println(softReference.get());
      System.gc();
      System.out.println(softReference.get());

      byte[] bytes = new byte[1024 * 1024 * 10];
      System.out.println(softReference.get());

      //// 将内存调小 -Xmx20M ,输出结果:

      [B@11d7fff
      [B@11d7fff
      null
    • 什么时候会被回收?

      1
      当JVM内存不足,执行GC后,内存依然不足,会回收软引用.
  • 什么是弱引用WeakReference?

    • 特点

      1
      2
      3
      4
      5
      6
      7
      8
      9
      WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1]);
      System.out.println(weakReference.get());
      System.gc();
      System.out.println(weakReference.get());

      //输出结果:
      [B@11d7fff
      null

    • 什么情况下会被回收?

      1
      只要触发GC就会被回收.
  • 什么是虚引用PhantomReference?

    • 特点

      1
      NIO中,就运用了虚引用管理堆外内存,用于堆外内存的释放.
      • 看官方源码注释 : 本大神硬核翻译!希望你能懂

        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
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        public class PhantomReference<T> extends Reference<T> 

        /**
        * Phantom reference objects, which are enqueued after the collector
        * determines that their referents may otherwise be reclaimed. Phantom
        * references are most often used for scheduling pre-mortem cleanup actions in
        * a more flexible way than is possible with the Java finalization mechanism.
        *

        虚引用对象,在垃圾收集器决定这些引用可能被回收时入队,
        虚引用经常用于在“对象死亡后,对对象进行死亡讣告”
        是一种可能比Java直接回收的更灵活一种方式.


        * <p> If the garbage collector determines at a certain point in time that the
        * referent of a phantom reference is <a
        * href="package-summary.html#reachability">phantom reachable</a>, then at that
        * time or at some later time it will enqueue the reference.
        *
        //此段没有太大的意义

        * <p> In order to ensure that a reclaimable object remains so, the referent of
        * a phantom reference may not be retrieved: The <code>get</code> method of a
        * phantom reference always returns <code>null</code>.
        *
        // 为了保证 已确定回收对象 的规则, 虚引用不再返回该对象的引用.

        * <p> Unlike soft and weak references, phantom references are not
        * automatically cleared by the garbage collector as they are enqueued. An
        * object that is reachable via phantom references will remain so until all
        * such references are cleared or themselves become unreachable.
        *
        * @author Mark Reinhold
        * @since 1.2
        */
        //不像其他引用一样,在虚引用入队后不会自动被垃圾回收器清理,
        一个可达对象被虚引用所引用后会一直保持在列队,直到这些引用被清理或者这些引用变得不可达.



        =================================

        /**
        * Reference queues, to which registered reference objects are appended by the
        * garbage collector after the appropriate reachability changes are detected.
        *
        * @author Mark Reinhold
        * @since 1.2
        */
        当垃圾回收器在适合的时机检测到可达性发生改变时决定 将这些 引用 加入列队.

        public class ReferenceQueue<T> {

    • 什么情况下会被回收?

      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
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      class A{
      @Override
      protected void finalize() throws Throwable {
      System.out.println("A对象 被回收了");
      }
      protected void freeMem() throws Throwable {
      System.out.println("执行堆外内存释放");
      }
      }
      class B{

      @Override
      protected void finalize() throws Throwable {
      System.out.println("B对象 被回收了");
      }

      }

      public class PerformanceTest {

      final int a = 0;
      final int b =10;
      final static int c = 0;
      volatile int d = 0;
      volatile int e = 0;

      public static void main(String[] args) throws InterruptedException {

      PerformanceTest p = new PerformanceTest();
      //p.test(0,0);
      //打印当前对象内存分布
      //System.out.println(VM.current().details());
      //System.out.println(ClassLayout.parseClass(PerformanceTest.class).toPrintable());
      //虚引用测试
      p.phantomReferenceTest();

      }



      private void phantomReferenceTest() throws InterruptedException {

      ReferenceQueue queue = new ReferenceQueue();
      List<byte[]> bytes = new ArrayList<>();
      //强引用
      A a = new A();
      B b = new B();
      //虚引用
      PhantomReference<A> reference = new PhantomReference<A>(new A(),queue);
      //弱引用
      WeakReference<byte[]> weakReference = new WeakReference<byte[]>(new byte[1024*1024*10]);
      System.out.println("强引用GC回收前强引用====================="+a);
      System.out.println("强引用GC回收前弱引用:====================="+weakReference.get());


      //此线程触发GC 设置-Xmx20M
      new Thread(() -> {
      for (int i = 0; i < 30;i++ ) {
      bytes.add(new byte[1024 * 1024]);
      }
      }).start();
      Thread.sleep(1000);

      System.out.println("强引用GC回收后弱引用:====================="+weakReference.get());

      new Thread(() -> {
      while (true) {
      Reference poll = queue.poll();
      if (poll != null) {
      System.out.println("虚引用被回收了:" + poll);
      }
      if (poll == null) {
      System.out.println("内存中没有引虚拟引用了,被清理了");
      try {
      new A().freeMem();
      } catch (Throwable throwable) {
      throwable.printStackTrace();
      }
      return;
      }
      }
      }).start();

      System.out.println("强引用GC回收后强引用A:====================="+a);
      }
      }


      //输出结果: 执行结果有乱序,不影响,用序号标记解释

      1.强引用GC回收前强引用=====================com.mybrainsbox.performance.A@621be5d1
      2.强引用GC回收前弱引用:=====================[B@573fd745
      3.A对象 被回收了
      4.强引用GC回收后弱引用:=====================null
      5.强引用GC回收后强引用A:=====================com.mybrainsbox.performance.A@621be5d1
      6.虚引用被回收了:java.lang.ref.PhantomReference@6d39548b
      7.内存中没有引虚拟引用了,被清理了
      8.B对象 被回收了
      9.执行堆外内存释放
      10.A对象 被回收了
      ====================================================

      3.A对象 被回收了 这一步输出对应 :
      PhantomReference<A> reference = new PhantomReference<A>(new A(),queue);
      一旦触发GC, 被PhantomReference引用的对象都会被设置为弱引用,并加入虚引用列队.等待回收.
      所以这一步在GC触发时输出.


      6.虚引用被回收了:java.lang.ref.PhantomReference@6d39548b
      这一步是 虚引用列队在GC后拿到被虚引用引用的new A()最后的回执,所有的可达性检测决定可能要回收的对象都会被加入此列队,直到poll()出全部.

      7.内存中没有引虚拟引用了,被清理了;
      这一步是poll出了所有的虚引用的对象列队;


      8.B对象 被回收了
      10.A对象 被回收了
      8和10是方法出栈被回收的.

怎么样找到垃圾?

  • 引用计数器

    1
    在对象头里,有数据位表示该对象有没有引用,当引用计数为0对象将被回收.
    • 缺陷 :循环引用 容易引起孤岛效应—- 即 : A -> B -> C , 3个对象互相引用,计数不为0,但实际已经没有任何引用指向这三个对象.
  • Root Searching 根可达算法
    • 根路径
      • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。
      • 方法区中的类静态属性引用的对象。
      • 方法区中的常量引用的对象。
      • 本地方法栈中JNI(即一般说的Native方法)的引用的对象。

常见GC垃圾回收算法

  • 标记 -> 清除

    • 特点

      1
      2
      3
      4
      5
      1,标记过程:
      从GC root出发遍历所有对象,在可达对象头中的markeword进行标记.

      2,清理过程:遍历堆中对象,判断对象头markword是否存活标记,进行回收.

    • 缺陷

      • 效率问题:需要2遍扫描,标记和清除都需要遍历,效率不高;

      • 空间问题:标记清除后会产生大量不连续的内存水平,空间碎片太多会导致大内存对象无法生成而频繁进行 GC。

    • 适用堆区

      • Old
    • 触发GC类型

      • Major GC/老年代GC
  • 标记 -> 复制

    • 特点

      1
      2
      3
      4
      5
      1,标记 : 将年轻代的空间一分为三,[Eden],[ s1 ] ,[ s2 ],比例是:8:1:1 ,

      2,复制: 当使用 s1内存块 使用率达到阈值,s2空白,将存活对象复制到s2内存空间.

      3,清理 : 清理s1空间块. 通过这个方式避免了碎片化空间问题.
    • 缺陷

      • 效率问题:标记和清除都需要遍历,效率不高;
      • 空间问题:内存空间浪费,最大10%,
    • 适用堆区

      • Eden
    • 触发GC类型

      • MinorGC/年轻代GC
  • 标记 -> 压缩(整理)

    • 特点

      1
      2
      3
      4
      1,先标记


      2,压缩整理: 把可达对象往内存块偏移量折中的任意一端移动,达到空间整洁规整。
    • 缺陷

      • 效率问题:需要扫描2次,移动对象引用需要重新引用地址,,效率不如复制算法高效;
    • 适用堆区

      • Old
    • 触发GC类型
      • Major GC/老年代GC

GC垃圾回收器(算法的实现)

CMS

STW安全点

  • 编译器提前编译好安全区域标志位,通过对象oopmap映射,GC通过opmap表来判断线程是否到达对象安全点.
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
HotSpot 用的就是映射表,这个表叫 OopMap。

在 HotSpot 中,对象的类型信息里会记录自己的 OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据,而在解释器中执行的方法可以通过解释器里的功能自动生成出 OopMap 出来给 GC 用。

被 JIT 编译过的方法,也会在特定的位置生成 OopMap,记录了执行到该方法的某条指令时栈上和寄存器里哪些位置是引用。

这些特定的位置主要在:

循环的末尾(非 counted 循环)

方法临返回前 / 调用方法的call指令后

可能抛异常的位置

这些位置就叫作安全点(safepoint)。

那为什么要选择这些位置插入呢?因为如果对每条指令都记录一个 OopMap 的话空间开销就过大了,因此就选择这些个关键位置来记录即可。

所以在 HotSpot 中 GC 不是在任何位置都能进入的,只能在安全点进入。

至此我们知晓了可以在类加载时计算得到对象类型中的 OopMap,解释器生成的 OopMap 和 JIT 生成的 OopMap ,所以 GC 的时候已经有充足的条件来准确判断对象类型。

因此称为准确式 GC。

其实还有个 JNI 调用,它们既不在解释器执行,也不会经过 JIT 编译生成,所以会缺少 OopMap。

在 HotSpot 是通过句柄包装来解决准确性问题的,像 JNI 的入参和返回值引用都通过句柄包装起来,也就是通过句柄再访问真正的对象。

这样在 GC 的时候就不用扫描 JNI 的栈帧,直接扫描句柄表就知道 JNI 引用了 GC 堆中哪些对象了。

安全点

我们已经提到了安全点,安全点当然不是只给记录 OopMap 用的,因为 GC 需要一个一致性快照,所以应用线程需要暂停,而暂停点的选择就是安全点。

我们来捋一遍思路。首先给个 GC 名词,在垃圾收集场景下将应用程序称为 mutator 。

一个能被 mutator 访问的对象就是活着的,也就是说 mutator 的上下文包含了可以访问存活对象的数据。

这个上下文其实指的就是栈、寄存器等上面的数据,对于 GC 而言它只关心栈上、寄存器等哪个位置是引用,因为它只需要关注引用。

但是上下文在 mutator 运行过程中是一直在变化的,所以 GC 需要获取一个一致性上下文快照来枚举所有的根对象。

而快照的获取需要停止 mutator 所有线程,不然就得不到一致的数据,导致一些活着对象丢失,这里说的一致性其实就像事务的一致性。

而 mutator 所有线程中这些有机会成为暂停位置的点就叫 safepoint 即安全点。

openjdk 官网对安全点的定义是:

A point during program execution at which all GC roots are known and all heap object contents are consistent. From a global point of view, all threads must block at a safepoint before the GC can run.





在JIT执行方式下,JIT编译的时候直接把Safepoint的检查代码加入了生成的本地代码。当JVM需要让Java线程进入Safepoint时,只需要设置一个标志位,让Java线程运行到Safepoint时主动检查这个标志位,如果标志被设置,那么线程停顿,如果没有被设置,那么继续执行。如HotSpot在x86中为轮询Safepoint会生成一条类似于test汇编指令。

image-20211112163726225

什么时候触发GC

  • l