文章目录
- 1. 概述
- 2. 引用计数算法
- 3. 可达性分析算法
- 4. 引用的分类
- 4.1 软引用的应用
本文参考:深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
1. 概述
总所周知,垃圾收集器的任务就是将“死去”的对象回收。
那么,垃圾收集器在对堆进行回收前,第一件事,就是要确认这些对象是否还“存活”着。
这里有两种算法:
- 引用计数算法
- 可达性分析算法
2. 引用计数算法
引用计数算法就是在对象中添加一个引用计数器,每当有一个地方引用它的时候,计数器就加一;当引用失效的时候,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。因此垃圾收集器就可以将此对象回收。
垃圾收集器虽然需要占用额外的内存来进行计数,但是它的原理简单,判定效率也很高。不过Java虚拟机并没有选用引用计数算法来管理内存。
这是因为单纯的引用计数很难解决对象之间相互循环依赖的问题。
比如有两个对象A和B,A对象引用B对象,B对象引用A对象,除此之外再无任何引用,但是实际上这两个对象不可能再被使用。如果使用引用计数算法,因为它们两个对象相互引用着对象,所以导致引用计数器都为1,因此垃圾收集器无法回收它们。
3. 可达性分析算法
可达性分析算法的思路就是通过一系列称为“GC Roots”
的跟对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索的过程所走过的路径称为“引用链”。
如果某对象到GC Roots
间没有任何引用链相连,或者用图论的话来说从GC Roots
到这个对象不可达,那么则证明此对象不可能再被使用
图中object5
与object6
和object7
虽然有关联,但是它们到GC Root
不可达,因此可以判定为是可回收对象。
在Java中,固定可以作为GC Roots
的对象包括以下几种
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,比如Java类的引用类型静态变量。
- 在方法区中常量引用对象,比如
StringTable
里的引用。 - 在本地方法栈中的JNI(也就是Native方法)引用的对象。
- Java虚拟机内部的引用,比如基本数据类型对应的Class对象,一些异常对象(
NullPointException
等),还有系统类加载器。 - 所有被同步锁(
synchronized
关键字)持有的对象。 - 反映Java虚拟机内部的情况
JMXBean
、JVMTI
中注册的回调、本地代码缓存等。
除了这些,还可以根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整的GC Roots
4. 引用的分类
在JDK1.2之前,如果reference
类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference
数据是代表某块内存、某个对象的引用。
在JDK1.2之后,引用分为强引用、软引用、弱引用和虚引用。
- 强引用
- 指的是程序代码中的引用赋值,比如
Object obj = new Object()
,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
- 指的是程序代码中的引用赋值,比如
- 软引用
- 软引用描述的是一些还有用,但非必须的对象
- 被软引用关联的对象,在系统将要发生内存溢出异常时(内存不住),触发垃圾回收。在垃圾回收之后,仍然内存不足,那么如果该对象没有被强引用,就会将这些对象进行回收。也就是第二次回收。
- JDK1.2之后提供了
SoftReference
实现软引用 - 可以配合队列来释放软引用本身
- 弱引用
- 弱引用也是用来描述那些非必须的对象
- 被弱引用关联的对象只能生存到下一次垃圾收集发生为止,无论内存是否足够,都会回收只被弱引用关联的对象。
- JDK1.2之后提供了
WeakReference
实现弱引用 - 可以配合队列来释放弱引用本身
- 虚引用
- 主要配合
ByteBuffer
使用,被引用对象回收时,会将虚引用入队,由Reference Handler
线程调用虚引用相关方法释放直接内存。详情可看【JVM】详解直接内存_起名方面没有灵感的博客-CSDN博客 - JDK1.2之后提供了
PhantomReference
类来实现虚引用
- 主要配合
- 终结器引用
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由
Finalizer
线程通过终结器引用找到被引用对象并调用它的finalize
方法,第二次 GC 时才能回收被引用对象
- 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由
4.1 软引用的应用
前提,添加VM Options
——-Xmx20m -XX:+PrintGCDetails -verbose:gc
-XX:+PrintGCDetails -verbose:gc
可以将垃圾回收的信息打印出来
private static final int _4MB = 10 * 1024 * 1024;
public static void main(String[] args) throws IOException {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}
System.in.read();
}
执行上面代码的时候,很明显会出现堆内存空间不足,因此会抛出OutOfMemoryError
但是改用软引用的话,就不会出现堆空间不足的情况了。
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
看控制台不难看出,在内存不足触发垃圾回收的时候,并不会第一时间将软引用关联的对象回收,而是在第二次垃圾回收和内存仍然不足的情况下,才会将软引用的对象回收。
可以使用队列将软引用移除
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();
// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}
System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}