抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

JVM垃圾回收机制

哪些内存需要回收

​ 由于程序计数器、虚拟机栈、本地方法栈的生命周期都跟随线程的生命周期,当线程销毁了,内存也就回收了,所以这几个区域不用过多地考虑内存回收。由于堆和方法区的内存都是动态分配的,而且是线程共享的,所以内存回收主要关注这部分区域。

​ 垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法

对象引用

传统定义:Reference中存储的数据代表的是另一块内存的起始地址。

​ 在 JDK 1.2 以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(Reachable)状态,程序才能使用它。从 JDK 1.2 版本开始,把对象的引用分为 4 种级别,从而使程序能更加灵活地控制对象的生命周期。无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关,这 4 种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

强引用(StrongReference)

​ 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。比如下面这段代码:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
new Main().fun1();
}

public void fun1() {
Object object = new Object();
Object[] objArr = new Object[1000];
}
}

​ 当运行至 Object[] objArr = new Object[1000]; 这句时,如果内存不足,JVM 会抛出 OOM 错误也不会回收 object 指向的对象。不过要注意的是,当 fun1 运行完之后,object 和 objArr 都已经不存在了,所以它们指向的对象都会被 JVM 回收。如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。比如 Vector 类的 clear 方法中就是通过将引用赋值为 null 来实现清理工作的:

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
/**
* Removes the element at the specified position in this Vector.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the Vector.
*
* @throws ArrayIndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
* @param index the index of the element to be removed
* @return element that was removed
* @since 1.2
*/
public synchronized E remove(int index) {
modCount++;
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
Object oldValue = elementData[index];

int numMoved = elementCount - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--elementCount] = null; // Let gc do its work

return (E)oldValue;
}

软引用(SoftReference)

​ 软引用是用来描述一些有用但并不是必需的对象,在 Java 中用 java.lang.ref.SoftReference 类来表示。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。因此,这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:

1
2
3
4
5
6
7
8
9
import java.lang.ref.SoftReference;

public class Main {
public static void main(String[] args) {

SoftReference<String> sr = new SoftReference<String>(new String("hello"));
System.out.println(sr.get());
}
}

弱引用(WeakReference)

​ 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.ref.WeakReference;

public class Main {
public static void main(String[] args) {

WeakReference<String> sr = new WeakReference<String>(new String("hello"));

System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}

输出结果为:

1
2
hello
null

​ 第二个输出结果是 null,这说明只要 JVM 进行垃圾回收,被弱引用关联的对象必定会被回收掉。不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

虚引用(PhantomReference)

​ “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在 Java 中用 java.lang.ref.PhantomReference 类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

1
2
3
4
5
6
7
8
9
10
11
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;


public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}

对象存活性判断

引用计数算法

​ 引用计数器在微软的 COM 组件技术中、Adobe 的 ActionScript3 种都有使用。引用计数器的原理很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。引用计数器的实现也非常简单,只需要为每个对象配置一个整形的计数器即可。但是引用计数器有一个严重的问题,即无法处理循环引用的情况。因此,在 Java 的垃圾回收器中没有使用这种算法。一个简单的循环引用问题描述如下:有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。但是在系统中却不存在任何第 3 个对象引用了 A 或 B。也就是说,A 和 B 是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

​ 引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

优缺点
  • 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

  • 缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0

可达性分析算法

​ 可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

​ 所谓的引用树本质上是有根的图结构,它沿着对象的根句柄向下查找到活着的节点,并标记下来;其余没有被标记的节点就是死掉的节点,这些对象就是可以被回收的,或者说活着的节点就是可以被拷贝走的,具体要看所在 HeapSize中 的区域以及算法,它的大致示意图如下图所示(注意这里是指针是单向的):

​ 首先,所有回收器都会通过一个标记过程来对存活对象进行统计。JVM 中用到的所有现代 GC 算法在回收前都会先找出所有仍存活的对象。下图中所展示的JVM中的内存布局可以用来很好地阐释这一概念:

​ 而所谓的GC根对象包括:当前执行方法中的所有本地变量及入参、活跃线程、已加载类中的静态变量、JNI 引用。接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从 GC 根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。

​ 不过那些发现不能到达 GC Roots 的对象并不会立即回收,在真正回收之前,对象至少要被标记两次。当第一次被发现不可达时,该对象会被标记一次,同时调用此对象的 finalize()方法(如果有);在第二次被发现不可达后,对象被回收。利用 finalisze() 方法,对象可以逃离一次被回收的命运,但是只有一次。逃命方法如下,需要在 finalize() 方法中给自己加一个 GCRoots 中的 hook:

1
2
3
4
5
6
7
8
public class EscapeFromGC(){
public static EscapeFromGC hook;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
EscapeFromGC.hook = this;
}
GC Roots对象

在Java语言中,可作为GC Roots的对象包括下面几种:

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

方法区的回收

Java虚拟机规范中描述可以不要求虚拟机在方法区实现垃圾收集,因此很多人认为方法区中是没有垃圾收集的。

​ 不要求虚拟机对方法区进行垃圾收集的原因主要是性价比比较低,在堆中,尤其是新生代中,进行一次垃圾收集一般会回收70%~95%的空间,但方法区的垃圾收集率远低于此。

​ 即使这样,对方法区进行垃圾收集也并非没有必要,在大量使用反射、动态代理等这类频繁定义ClassLoader的场景都需要虚拟机卸载类的功能,以保证方法区不会溢出。

​ 方法区的垃圾收集主要回收废弃常量与无用的类。

​ 废弃常量的判定与回收比较简单:以“abc”这个常量为例,如果当前系统中没有任何对象引用这个常量,也没有任何其他地方(博主猜测是.class文件中有些地方对此常量的引用)引用这个字面量。此时如果发生内存回收,这个常量就会被清理出常量池。(常量池中其他类、接口、方法、字段的符号引用与此类似)

​ 方法区存储内容是否需要回收的判断可就不一样咯。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述三个条件的无用类进行回收。

HotSpot 算法实现

枚举根节点

​ 在可达性分析法中对象能被回收的条件是没有引用来引用它,要做到这点就需要得到所有的GC Roots节点,来从GC Root来遍历。可作为GC Root的主要是全局性引用(例如常量和静态变量),与执行上下文(栈帧中的本地变量表)中。那么如何在这么多的全局变量和栈中的局部变量表中找到栈上的根节点呢?

​ 在栈中只有一部分数据是Reference(引用)类型,那些非Reference的类型的数据对于找到根节点没有什么用处,如果我们对栈全部扫描一遍这是相当浪费时间和资源的事情。

​ 那怎么做可以减少回收时间呢?我们很自然的想到可以用空间来换取时间,我们可以在某个位置把栈上代表引用的位置记录下来,这样在gc发生的时候就不用全部扫描了,在HotSpot中使用的是一种叫做OopMap的数据结构来记录的。对于OopMap可以简单的理解是存放调试信息的对象。

​ 在OopMap的协助下,我们可以快速的完成GC Roots枚举,但我们也不能随时随地都生成OopMap,那样一方面会需要更多的空间来存放这些对象,另一方面效率也会简单低下。所以只会在特定的位置来记录一下,主要是正在:

  1. 循环的末尾
  2. 方法临返回前/调用方法的call指令后
  3. 可能抛异常的位置

这些位置称为安全点。

安全点

​ 我们在做GC的时候需要让jvm停在某个时间点上,如果不是这样我们在分析对象间的引用关系的时候,引用关系还在不断的变化。这样我们的准确性就无法得到保证。 安全点就是所有的线程在要GC的时候停顿的位置。那么如何让所有的线程都到安全点上在停顿下来呢?这里有两种方案可以选择:

  • 抢先式中断
  • 主动式中断

​ 在抢先式中断:中不需要线程主动配合,在GC发生的时候就让所有线程都中断,如果发现哪个线程中断的地方不在安全点上,那么就恢复线程,然后让它跑到安全点上。

​ 主动式中断是:让GC在需要中断线程的时候不直接对线程操作,设置一个标志,让各个线程主动轮询这个标志,如果中断标志位真时就让自己中断。

什么时候回收

finalize方法

​ 通过上面几种算法,虚拟机可以知道此时内存中有哪些需要被回收的对象,但是虚拟机什么时候会对这些对象进行回收呢?我们需要来谈一谈finalize方法。

​ 在JVM中,当一个对象通过可达性分析算法被判定为垃圾的时候,JVM并不能直接对其进行回收,一是垃圾回收机制并不是实时进行,二是真正的回收一个对象之前还会判断是否要运行它的finalize方法。

​ 当一个对象被判定为是垃圾之后,它将会被第一次标记并进行一次筛选,筛选的条件就是此对象是否有必要执行finalize方法。

如何判断一个对象是否有必要执行finalize方法呢?

两种情况下虚拟机会视为“没有必要执行”:

  • 对象没有覆盖finalize方法
  • finalize方法已经被虚拟机调用过(finalize方法只会被调用一次)

​ 如果这个对象被判定为有必要执行finalize方法,那么这个对象会被放置在一个叫做F-Queue的队列之中,并在稍后由一个被虚拟机创建的,低优先级的Finalizer线程去执行该对象的finalize()方法,并且对象在finalize()方法执行中如果出现执行缓慢或者发生死循环,将会导致F-Queue队列中其他对象永久处于等待。甚至导致整个内存回收系统崩溃。之后GC将会对F-Queue之中的对象进行第二次标记。**如果在第二次标记前这些对象在自己的finalize()方法中可以拯救自己(重新与引用链上的任何一个对象建立关联即可)**也是可以成功存活下来并被移除“即将回收”的集合的。 如果此时还没有逃脱,那就真的要被回收了。

​ 注意:finalize()方法的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。博主建议大家完全可以忘掉Java语言中有这个方法的存在。

请忘记 finalize

​ finalize可以完成对象的拯救,但是JVM不保证一定能执行,所以请忘记这个“坑”。

对象死亡前的最后一次挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记

​ 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记。

第二次标记

​ 第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

​ 第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

评论