概述
垃圾收集(Garbage Collection,下文简称GC)需要完成的三件事情:哪些内存需要回收?什么时候回收?如何回收?
第2章介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域的生命周期都是确定的,随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而执行对应的入栈和出栈操作。同时,这几个区域所需的空间大小都是确定的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑如何回收的问题:当方法结束或者线程结束时,内存自然就跟随着回收了。
而方法区与java堆则是动态且具备不确定性的:
- 方法区:
- 在运行时动态加载类
- java堆:
- 对象实例在这里动态分配内存。对象的数量、大小(实现动态绑定时,不同的接口实现类实例可能需要不同大小的内存)以及生命周期在编译期间是不确定的,只能在程序运行时动态确定。
垃圾收集器所关注的正是这部分内存该如何管理,本文后续讨论中的内存
分配与回收也仅仅特指这一部分内存。
1 对象死亡判断
垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。这主要涉及到两种算法。
1.1 引用计数算法(Reference Counting)
- 原理:
- 每个对象有一个引用计数器,引用该对象时计数器增加,引用失效时计数器减少。计数器为零意味着对象不再被使用。
- 优势:
- 引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。
- 限制:
- 很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作。譬如无法解决对象间的循环引用问题:两个对象相互引用,即使它们不再被其他对象引用,它们的引用计数也不为零,从而无法被回收。
1.2 可达性分析算法(Reachability Analysis)
- 原理:
- 通过一系列称为“GC Roots”的根对象作为起始点,如果某个对象到GC Roots没有任何引用链(该算法根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain))相连,则该对象不再被使用。
- GC Roots范围:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
- 优势:
- 能有效处理循环引用的情况,是目前主流的垃圾收集算法。
2 引用
JDK 1.2及之后版本中Java对引用概念进行了扩展,引入了强引用、软引用、弱引用和虚引用这四种不同类型的引用(强度依次递减)。
2.1 强引用(Strong Reference)
- 定义:传统意义上的引用,例如
Object obj = new Object();
这类的引用关系。 - 特点:只要强引用关系存在,垃圾收集器永远不会回收被引用的对象。
2.2 软引用(Soft Reference)
- 定义:用来描述一些有用但并非必需的对象。
- 特点:
- 当内存足够时,软引用关联的对象不会被回收。
- 当内存不足即将抛出内存溢出异常之前,这类对象会被考虑进行回收。
- JDK中提供了
SoftReference
类来实现软引用。
2.3 弱引用(Weak Reference)
- 定义:比软引用更弱的一种引用关系。
- 特点:
- 只能生存到下一次垃圾收集之前。
- 无论内存是否足够,只被弱引用关联的对象都会被回收。
- JDK中提供了
WeakReference
类来实现弱引用。
2.4 虚引用(Phantom Reference)
- 定义:最弱的一种引用关系,也称为“幽灵引用”或“幻影引用”。
- 特点:
- 对象的生存时间与虚引用无关。
- 虚引用主要用于跟踪对象被垃圾收集器回收的活动。
- 无法通过虚引用获取对象实例。
- JDK中提供了
PhantomReference
类来实现虚引用。
3 finalize()方法与对象的自救
- 可达性分析:在垃圾收集过程中,首先通过可达性分析确定哪些对象是“存活”的,哪些是“死去”的。不可达的对象可能会被垃圾收集器回收。
- Finalize机制:
- 在对象被判定为不可达后,它们不会立即被回收,而是进行第一次标记和筛选,筛选条件是该对象是否有必要执行finalize()方法(假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用 过,那么虚拟机将这两种情况都视为“没有必要执行”,如没有必要执行则直接回收)。
- 对象的finalize()方法是对象被回收前来“自救”的最后且唯一的机会(即重新变为可达)。
- Finalizer线程:
- 被判定需要执行finalize()方法的对象会被放入F-Queue队列。
- Finalizer线程低优先级地执行这些对象的finalize()方法。
- 第二次标记:
- 在finalize()执行后,垃圾收集器会对F-Queue中的对象进行第二次小规模标记。
- 如果对象在finalize()后重新变为可达,则它将被移出待回收集合,即自救成功。
3.1 使用建议
- 不推荐使用finalize():由于finalize()方法存在诸多问题(如不确定性、高开销、执行顺序不确定等),官方并不推荐使用。
- 替代方案:使用try-finally或其他方式都可以完全代替finalize(),且做的更好,更及时。
4 方法区的垃圾收集
方法区的垃圾收集通常回收效果不如Java堆,特别是与新生代的垃圾收集相比。虽然《Java虚拟机规范》允许虚拟机不在方法区实现垃圾收集,但实际上许多JVM实现(如HotSpot)确实存在对方法区进行的垃圾收集,这主要涉及到两方面。
-
废弃常量的回收:与对象回收类似。例如,不再被引用的字符串常量可以从常量池中被回收。
-
不再使用的类型的回收:这部分更复杂,需要同时满足三个条件:
-
该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
-
加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
-
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
并且即使满足上述三个条件也只是“被允许”,具体回收与否则可通过相关参数控制。
-