Java的GC机制

目的

  • 自动回收防止内存泄漏
  • 提高效率
  • 提高稳定性

一、需要解决的问题

​ GC机制从不是和Java一起问世的,而是通过了很长时间的发展终于找到了Java这个载体。在很早以前,Lisp语言的创始人开始思考垃圾回收,并且提出了三个问题:

  • 哪些内存需要回收
  • 什么时候需要回收
  • 如何回收

二、 哪些内存需要回收?

​ 在GC中,主要需要回收的是两个区域:Java堆和方法区。因为程序在运行时所需要的数据,存放的区域如:程序计数器、虚拟机栈、本地方法都是随线程生、随线程死。而我们的Java堆和方法区存放的数据需要动态的进行回收,因为我们不知道程序会创建多少对象,因为如果接口的实现类大不相同,并且不知道有些ifelse分支所需要的空间时多少,所以我们只有在运行过程中才能得知。所以这些内存是需要区回收的。

三、什么时候需要回收?

​ 这个问题的答案很简单,就是正在存活的的对象不能被回收,已经“死去”(不可能再被任何途径使用的对象)的对象需要被回收。可是如何判断是否是正在存活的呢?在过去的发展中,有很多的算法来解决这个问题

  1. 引入计数法:这是一个经典的算法,在对象中添加一个引用计数器,每当有一个地方引用它的时候就+1,当引用失效的时候就-1。任何时刻当计数器为0的时候,就是这个对象不可能在被使用的时候。但在Java中,我们很少使用这个算法,因为引入计数法虽然看似简单,但是需要处理很多额外的情况,并不只是单一的+1和-1的问题,例如两个对象的循环引用中,它不能很好的起作用。
  2. 可达性分析算法:这是Java和C#语言一直使用的判断算法,也就是设置一个GC Roots起始节点,然后如果可以通过向下搜索的引用关系来搜索所需的对象,那么这条路径就叫“引用链”,如果一个对象到GC Roots没有任何引用链(也称不可达),那么我们就称这个对象不可能在被使用,例如下图的Object 5、6、7虽然相互有关联,但是由于它们没法到达GC Roots,所以它们是可被回收的

最后要解释一下什么是引用,一般引用分为四种类型:强引用、软引用、弱引用、虚引用,每一种引用都代表了对象的一些状态。强引用就是new出来的对象,这种类型的关系是不可能被垃圾回收的。软引用是指还有用但是非必须的对象。如果内存即将异常的时候会在下一次垃圾回收的时候回收软引用对象。弱引用是指非必须的对象,只能存活到下一次垃圾回收发送为止。而虚引用是一定被回收的,只是定义一个这个来通知系统要进行垃圾回收。下面是一个关于引用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package GC;
import java.lang.ref.*;

public class ReferenceTypes {
public static void main(String[] args) {
Object strongRef = new Object(); // 强引用
SoftReference<Object> softRef = new SoftReference<>(new Object()); // 软引用
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>()); // 虚引用

System.gc(); // 触发垃圾回收

System.out.println("Strong Reference: " + strongRef);
System.out.println("Soft Reference: " + softRef.get());
System.out.println("Weak Reference: " + weakRef.get());
System.out.println("Phantom Reference: " + phantomRef.get());
}
}
result :
Strong Reference: java.lang.Object@723279cf
Soft Reference: java.lang.Object@2f4d3709
Weak Reference: null
Phantom Reference: null

三、如何回收

​ 这是一个非常庞大的问题,前面的介绍只是仅仅是一些基础。对于一个对象来说,想要使它完全被回收,光是判断一次是不够的,一般来说我们经常会判断两次:第一次是指通过可达性分析是否与GC Roots相连,如果不相连,第二次我们还可以看看这个对象是否有必要执行finalize()方法,如果判定为有必要执行,就把这个对象放入一个叫F-Queue的队列,然后等待执行finalize()方法。如果对象不想在finalize()方法执行结束以后消亡,就需要重新与引用链上的任何一个对象建立关联即可

在 Java 中,finalize() 方法是 java.lang.Object 类中的一个方法,它允许对象在垃圾回收器回收该对象使用的内存之前执行必要的清理操作。这个方法的设计初衷是为对象提供一个机会,以便在对象被垃圾回收之前关闭文件、数据库连接或其他非托管资源finalize()函数其实就是GC去执行的一个函数,执行结束了对象就会被清除。任何一个对象的finalize()方法只会调用一次。

1. 方法区的回收判断

​ 前面提到过,方法区一般不会被回收,但是存在一些很严苛的条件我们才会回收方法区。因为方法区存放的是常量和一些被加载进来的类型的类信息。想要常量被回收是非常容易的,要满足两个条件:

  • 没有字符串对象引用常量池中的常量了。
  • 没有其他地方引用这个字面量。

​ 但对于一些类型想要判断是否还要继续使用是比较苛刻的,具体需要满足三个条件:

  • 这个类的所有实例都已经被回收了(包括任何派生子类的实例)
  • 这个类的类加载器已经被回收了。
  • 这个类的java.lang.Class对象没有在任何地方被引用。所以通过反射是无法达到这个对象的。
1
java -XX:+TraceClassUnLoading // 查看类加载和卸载信息
  1. 正式回收算法

分代收集(Generational Collection)理论:分代收集理论认为绝大多数对象都是朝生夕灭的,并且熬过越多次垃圾收集过程的对象就越难消亡。所以在分代收集理论的眼里,Java堆必须划分不同的区域,然后回收的对象根据年龄(熬过垃圾收集过程的次数)分配到不同的区域存储。

如果每次分代回收之回收其中某一个区域就产生了:”Minor GC” “Major GC” “Full GC”,也发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”

Minor GC:目标只是新生代的垃圾收集

Major GC:目标只是老生代的垃圾收集

Full GC: 收集整个Java堆和方法区的垃圾收集

标记-清除算法 :算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程。

pkceZP1.png

缺点

​ 1. 执行效率不稳定 :大量标记和清除的动作

​ 2.内存空间的碎片化问题:标记-清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

标记-复制算法 :将可用内存按容量划分为不同大小或者对半的区域,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

一般设置三块区域:Eden区域、Survivor区域、Survivor区域。比例是8:1:1。所有的新生代老生代在一块survivor区域和Eden区域。然后回收后把无法回收的对象放到另一块Survivor区域。

pkceYGt.png

标记-整理算法:其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

缺点移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。

pkce7J1.png