jvm

JAVA垃圾回收

Posted by Shenpotato on June 24, 2021

参考:

《深入理解java虚拟机 : JVM高级特性与最佳实践(第2版)》》周志明

从三个方面来阐述jvm的垃圾回收。

  • 什么样的对象需要回收
  • 什么时候回收
  • 怎么回收

一、什么样的对象需要回收

1. 引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,则可以回收该对象。

存在问题:

无法解决循环引用的问题。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LoopReference{
  private Object instance;
  
  public void setInstance(Object instance) {
    this.instance = instance;
  }
  
  public static void main(){
    LoopReference instance1 = new LoopReference();
    LoopReference instance2 = new LoopReference();
    instance1.setInstance(instance2);
    instance2.setInstance(instance1);
    
    instance1 = null;
    instance2 = null;
    
    System.gc();
  }
}

2. 可达性分析

设定一个对象作为GC Root(起点),从这个节点开始向下搜索,所走过的路径称为引用链。当一个对象到GC Root没有任何引用链相连时(GC Root到这个对象不可达),回收该对象。

2.1 可以作为GC Root对象

  • 虚拟机栈(栈帧中的本地变量表)中所引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

2.2 引用类型

  • 强引用:只要引用存在,永远不会回收

    1
    
    Object object = new Object();
    
  • 软引用:有用但非必需的对象。

    系统发生内存溢出异常之前,对软引用进行二次回收。如果此次回收仍然没有足够内存,则抛出内存溢出异常。通过SoftReference实现。

  • 弱引用:描述非必需对象

    被弱引用关联的对象只能生存到下一次垃圾回收发生之前。换句话说,垃圾回收必回收。通过WeakReference实现。

  • 虚引用:幽灵引用/幻影引用

    一个对象是否有虚引用,不会对其生存时间产生影响,也无法通过虚引用获得实例。设置虚引用的目的是为了能在这个对象被垃圾回收之前收到一个系统通知。通过PhantomReference实现。

2.3 对象生存or死亡

针对不可达的对象,需要进行两次的标记过程。当一个对象GC Root不可达时,进行一次标记,并且进行一次筛选。筛选条件是该对象是否有必要执行finalize()方法。有必要执行finalize()方法有两个条件:

  • 重写了finalize()方法
  • finalize()方法未被执行过。即是第一次进行标记。

因此,一个对象要逃脱被回收,需要重写finalize()方法,并且,finalize()方法里需要重新与引用链上的对象实现关联。当然,如果被拯救的对象再被垃圾回收,此对象就无法逃脱。

《深入理解java虚拟机 : JVM高级特性与最佳实践(第2版)》》周志明存在代码实现,可以看看。

2.4 对象回收的区

  • 堆:新生代最常发生GC,回收率会达到85%-90%,未被回收的对象进行老生代(Survivor)

  • 方法区中的常量池:

    回收方法与堆中方法类似。例如通过string.intern()的方法在常量池中创建了对象,但是后续未被引用。

  • 方法区中的类信息:

    无用的类:

    • 该类所有实例已被回收(堆中不存在任何实例)
    • 加载该对象的ClassLoader已被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用(没有通过反射引用的地方)

    tips:无用的类可以被回收,但不一定会被回收。

二、垃圾回收方法

1. 常见回收方法思想

1.1 标记-清除

标记处需要回收的对象,然后清除。

缺点:标记和清除效率不高;清除后会产生大量的不连续空间碎片,无法为对象分配大的内存而重新GC。

1.2 复制

将容量进行划分为两块,每次使用其中一块。当某块容量满后, 将其存活对象复制到另一块上。

优点:不考虑内存碎片的情况,只需要移动堆顶指针,顺序分配,实现简单运行高效

缺点:

  1. 浪费内存

    针对缺点和大量的对象是需要经常回收的特点,将堆的新生代分为一块大的Eden和两块Survivor。当回收时,将Eden和From Survivor中的对象复制到To Survivor中,并清理空间。大概比例是8:1:1。当Survivor空间不够时,需要从堆的老生代去借。

  2. 但在对象存活率较高时需要进行较多的复制操作,效率降低
  3. 如果不想浪费50%空间,就需要有其他空间进行担保(即可以去借)。所以在老年代不采取复制算法。

1.3 标记-整理

同标记清除,区别是在标记后,使所有存活对象向一边移动,直清理掉端边界以外内存。

1.4 分代收集

堆中分为新生代,新生代采用复制,老生代采用标记整理 or 标记清除。

2. 具体实现