JVM学习笔记(四):GC算法与四种引用

214 阅读5分钟

1 来源

  • 来源:《Java虚拟机 JVM故障诊断与性能优化》——葛一鸣
  • 章节:第四章

本文是第四章的一些笔记整理。

2 GC算法

常见的GC算法包括:

  • 引用计数法
  • 标记清除法
  • 复制算法
  • 标记压缩法
  • 分代算法
  • 分区算法

2.1 引用计数法

实现原理:

  • 对于一个对象A
  • 只要有任何一个对象引用A,A的引用计数器就加1
  • 引用失效时,引用计数器就减1
  • 只要对象A的引用计数器的值为0,对象A就不会再被使用,等待被回收

缺点:

  • 无法处理循环引用,比如A引用B,B引用A,但是并没有其他对象引用两者,此时A和B的引用计数都不为0,这样就无法被回收
  • 引用计数器要求每次引用产生和消除的时候,伴随一个加法操作和一个减法操作,对系统性能会有一定的影响

由于引用计数法上述的缺点,Java中并未采用作为GC算法。

2.2 标记清除法

标记清除法将垃圾回收分为两个阶段:

  • 标记阶段:通过根节点标记所有从根节点开始的可达对象,未被标记的就是垃圾对象
  • 清除阶段:清除所有未被标记的对象

标记阶段:

在这里插入图片描述

清除后:

在这里插入图片描述

缺点很明显,就是回收后的空间是不连续的,工作效率会低于连续的内存空间。

2.3 复制算法

核心思想:

  • 内存空间分为相等的两块
  • 每次只使用其中一块
  • 回收的时候将存活对象移到另一块中,然后清除正在使用的内存块中所有对象
  • 交换两个内存块的角色

优点是回收后的内存空间是没有碎片的,而缺点是如果存在大量的对象,需要花费大量的时间复制,并且内存只有原来的一半。

比如下图中的A、B两块相同的内存空间,A在垃圾回收的时候,将存活对象复制到B中,B在复制后保持连续:

在这里插入图片描述

复制完成后,A会被清空,并将B设置为当前使用的空间。

Java的新生代串行垃圾回收器中,使用了复制算法,新生代分为eden区、from区以及to区。其中fromto区是两块内存相同的空间,也叫survivor区,也就是幸存者空间。在垃圾回收的时候,eden区以及from区存活的对象会被复制到to区,然后清空from区与eden区,接着fromto区的角色将会交换,也就是下一次垃圾回收的时候,会从原来的to区(新的from区)复制到原来的from区(新的to区)。

2.4 标记压缩法

标记压缩法是一种老年代算法,在标记清除法的基础上做了一些优化,和标记清除法一样,首先也需要从根节点开始,对所有可达对象做一次标记,然后将所有存活对象压缩到内存的一端,接着清理边界外的所有空间,图示如下:

在这里插入图片描述

标记压缩法的优点是可以避免碎片的产生,又不需要两块相同的内存空间。

2.5 分代算法

分代算法并不是一种具体的垃圾回收算法,分代算法其实是一种根据每块内存空间的特点使用不同回收算法以提高效率的算法。比如:

  • 在新生代中:会有大量的新建对象很快被回收,因此新生代比较适合使用复制算法
  • 在老年代中:采用标记压缩法或标记清除法

2.6 分区算法

分区算法将整个堆空间划分成连续的不同小区间,每个小区间都独立使用,独立回收,如图所示:

在这里插入图片描述

3 四种引用

Java里面提供了4个级别的引用:

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

下面分别来看一下。

3.1 强引用

强引用就是代码中一般使用的引用类型,强引用的对象是可触及的,不会被回收,比如:

StringBuffer str = new StringBuffer("a");

如果上面的代码运行在方法体内,那么局部变量str会被分配在栈上,而对象StringBuffer实例会被分配在堆上,str指向的是StringBuffer实例所在的堆空间,通过str可以操作该实例,str就是StringBuffer实例的强引用。

又比如执行了以下代码:

StringBuffer str1 = str;

那么str1也会指向str指向的对象,也就是它们都指向同一个StringBuffer实例,此时str1==str的值为真,因为两个指向的是同一个堆空间地址。

强引用的特点如下:

  • 可以直接访问目标对象
  • 强引用指向的对象不会被系统回收,JVM宁愿抛出OOM也不会回收强引用指向的对象
  • 强引用可能会导致内存泄漏

3.2 软引用

软引用是被强引用弱一点的引用类型,如果一个对象只持有软引用,那么当堆空间不足的时候,就会被回收,软引用可以使用SoftReference类实现,比如下面的代码:

public static void main(String[] args){
    Byte[] b = new Byte[1024*1024*8];
    SoftReference<Byte[]> softReference = new SoftReference<>(b);
    b = null;
    System.out.println(softReference.get());
    System.gc();
    System.out.println("After GC");
    System.out.println(softReference.get());
    b = new Byte[1024*1024*8];
    System.gc();
    System.out.println(softReference.get());
}

OpenJDK 11.0.10上,加上-Xmx40m的输出如下:

[Ljava.lang.Byte;@1fbc7afb
After GC
[Ljava.lang.Byte;@1fbc7afb
null

可以看到,当垃圾回收的时候,未必会回收软引用对象,但当内存紧张时,会回收软引用对象。

3.3 弱引用

弱引用是比软引用弱的引用类型,在垃圾回收的时候,只要发现弱引用,不管系统空间使用情况如何,都会将对象进行回收。但是由于垃圾回收器的线程通常优先级不高,并不一定能很快发现弱引用对象,这种情况下弱引用对象可以存在较长时间。弱引用例子如下:

public static void main(String[] args){
    Byte[] b = new Byte[1024*1024*8];
    WeakReference<Byte[]> softReference = new WeakReference<>(b);
    b = null;
    System.out.println(softReference.get());
    System.gc();
    System.out.println("After GC");
    System.out.println(softReference.get());
}

输出(-Xmx40m):

[Ljava.lang.Byte;@1fbc7afb
After GC
null

可以看到在GC后,弱引用对象会被立即回收。

软引用、弱引用的一个常见使用场景是保存可有可无的缓存数据,当系统内存不足时,这些内存数据会被回收,不会导致OOM,而内存充足时,这些缓存数据又可以存在相当长的时间,从而起到让系统加速的作用。

3.4 虚引用

虚引用是所有引用类型中最弱的一个,一个持有虚引用的对象和没有引用几乎是一样的,随时都可能被垃圾回收器回收。另外,试图使用虚引用的get()方法获取强引用的时候,总是会失败,并且虚引用需要和引用队列一起使用,作用在与跟踪垃圾回收过程。

public static void main(String[] args) throws Exception {
    ReferenceQueue<String> queue = new ReferenceQueue<>();
    PhantomReference<String> reference = new PhantomReference<>(new String("test"),queue);
    System.out.println(reference.get());
}

输出结果:

null