读书笔记——《深入理解 Java 虚拟机》系列之回收对象算法与四种引用类型

本贴最后更新于 2406 天前,其中的信息可能已经时移俗易

上一篇博客中,博主和大家一起学习了 Java 虚拟机运行时内存区域的划分:主要是线程私有的虚拟机栈,本地方法栈和程序计数器以及线程公有的虚拟机堆和方法区。

对于栈内存而言,每个栈帧所需的内存在类结构确定下来后基本已经确定了,栈中的栈帧随着方法的进入和退出不断进行入栈和出栈操作,换句话说栈中的内存分配具有确定性,当方法结束时栈中的内存也就自动释放了。至于程序计数器,它的生命周期与线程的生命周期相同,在线程结束时,内存也就自动释放了。虚拟机堆内存和方法区与它们并不相同,因为我们只有在程序运行时才知道会创建哪些对象,class 的加载是在运行时的,还有一些 class 是通过动态代理在运行时生成的,因此两个内存区域的内存分配和回收都是动态的。Java 的垃圾回收主要关注的就是这两块内存区域。

1.需要被回收的对象

在考虑 Java 虚拟机的垃圾回收之前,最重要的一点就是判断哪些对象应该被回收。

1.1 引用计数法

引用计数法通过给每个对象添加一个引用计数器,每当一个地方引用它时,它的计数器数值加 1;当引用失效的话,计数器数值减 1;任何时刻计数器数值为 0 的对象就是应该被回收的对象。这种算法简单实用,但是却很难解决对象之间的相互引用问题,如下所示:

public class ReferenceCountingGC{
  public Object instance = null;
  
  public static void main(){
	ReferenceCountingGC objA = new ReferenceCountingGC();   
	ReferenceCountingGC objB = new ReferenceCountingGC();
	objA.instance = objB;
	objB.instance = objA;
	objA = null;
	objB = null;
	
	//在这里通知虚拟机可以进行gc
	System.gc();
  }
  
}

上面的代码在虚拟机内存中以下图的方式所呈现:
6e7f07ad158e40759bc24cfaa3ad4a28.png
尽管我们在代码中将 objA 和 objB 指向了 null,由于这两个对象互相引用,它们的引用计数器的数值仍然不为 0,因此若使用这种引用计数法对象 X 和对象 Y 都没有办法被回收。但如果我们查看 Java gc 的日志,我们会发现,这两个对象的内存已经被回收了,这是因为 Java 采用了另一种算法来判断哪些对象该被回收。

1.2 可达性分析算法

Java 的 GC 机制是根据可达性分析算法(Reachability Analysis)来判定对象是否存活的。简单来说,这个算法通过一系列的“GC Roots”的对象作为起始点,从这些起始点开始向下搜索,搜索走过的路叫做引用链,当一个对象到 GC Roots 没有任何引用链时,证明此对象不可达,如下图中的 object5,object6,object7:
605c45a5d89e4408bec78a0d2937b4cd-ReachabilityAnalysis.jpg

那么在 Java 中哪些对象可以用被当作 GC Roots 呢?

  • 在虚拟机栈(栈帧中的局部变量表)中的对象引用:就像我们上面代码中的例子,起初在 main 方法中声明的两个对象引用 objA 和 objB 分别指向堆内存中的对象 X 和对象 Y,一旦我们将 objA 和 objB 两个局部变量引用指向了 null,对象 X 和对象 Y 即使互相引用但对于原本的 GC Roots(objA 和 objB)而言已经是不可达了,因此这两个对象所占的内存可以被回收,而不会像引用计数法,由于两个对象的计数器数值不为 0 导致不能被回收。
  • 在方法区中类的静态的对象引用:这些由 static 标识的静态变量引用也会被当作 GC 的根节点。
  • 在方法区中常量的对象引用:每个类都拥有一个常量池,这些常量池中也有一些对象的引用,这些常量引用也会被当作 GC 的根节点。
  • 在本地方法栈中持有的对象引用:一些对象在被传入到本地方法前,这些对象还没有被释放,此时这些对象引用也会被当作 GC 的根节点。
  • 方法区中类的 Class 对象引用:每个类被 JVM 加载时都会创建一个代表这个类的唯一的 Class 类型的对象,这个 Class 对象同样存放在堆中,当这个类不再被使用时,方法区中的类数据和在堆中的 Class 对象都需要被回收。因此,Class 对象的引用也会被当作 GC 的根节点。

2.Java 中的引用

无论是通过上述那种算法判断对象是否应该被回收,都和对象是否被引用相关。

在 Java 中,存在四种引用类型:

  • 强引用(Strong Reference):强引用是 Java 中实例化对象采用的默认的引用类型,如“Object o = new Object()”这类的强引用,只要强引用存在,垃圾收集器就不会回收掉被引用的对象。
  • 软引用(Soft Reference):软引用用来描述一些还有用但并非必需的对象。对于软引用指向的对象,在系统将要发生内存溢出之前,会将这些对象所占的内存回收,如果回收之后仍然没有足够的内存,才会抛出内存溢出异常。
	SoftReference sr = new SoftReference(new String("hello"));
	System.out.println(sr.get());`
  • 弱引用(Weak Reference):弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用指向的对象只能生存到下一次垃圾收集发生之前,一旦垃圾收集器开始工作,无论当前内存是否足够,都会回收掉被弱引用指向的对象。
	WeakReference<String> wr = new WeakReference<String>(new String("hello"));
        System.out.println(wr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(wr.get());
  • 虚引用(Phantom Reference):虚引用是最弱的一种引用关系。一个对象是否有虚引用对它是否会被回收完全没有影响,我们甚至不能通过虚引用来获得一个对象的实例。设置虚引用的唯一用处就是当该对象被回收时会收到一个系统通知。

下面博主给大家写一个,关于各种引用类型与内存回收的一个综合例子,来帮助大家更好地理解不同引用类型与垃圾回收的关系:

package com.wxueyuan.test;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class ReferTest {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		if(args.length!=0) {
			switch(args[0]) {
				case "strong" :
					strongReferenceTest();
					break;
				case "soft":
					softReferenceTest();
					break;
				case "weak":
					weakReferenceTest();
					break;
				case "phantom":
					phantomReferenceTest();
					break;
			}
		}
	}
	
	public static void strongReferenceTest() {
		List<ReferObject> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			//实例化ReferObject
			ReferObject obj = new ReferObject(i.toString());
			//将对象放入list中防止被垃圾回收
			list.add(obj);
			System.out.println(obj);
		}
	}
	
	public static void softReferenceTest() {
		SoftReference<ReferObject> sr = new SoftReference<ReferObject>(new ReferObject("obj"));
		System.out.println(sr.get());
		//通知jvm可以进行垃圾回收
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("after gc worked " +sr.get());
		System.out.println("***************************");
		
		List<SoftReference<ReferObject>> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			ReferObject obj = new ReferObject(i.toString());
			list.add(new SoftReference<ReferObject>(obj) );
			System.out.println(list.get(i-1).get());
			//每隔2s创建一个对象
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public static void weakReferenceTest() {
		WeakReference<ReferObject> wr = new WeakReference<ReferObject>(new ReferObject("obj"));
		//通知jvm可以进行垃圾回收
		System.out.println(wr.get());
		//等待gc工作
		System.gc();
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("after gc worked " +wr.get());
		System.out.println("***************************");
		
		List<WeakReference<ReferObject>> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			ReferObject obj = new ReferObject(i.toString());
			list.add(new WeakReference<ReferObject>(obj) );
			
			//每隔2s创建一个对象
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(list.get(i-1).get());
		}
	}
	
	public static void phantomReferenceTest() {
		ReferObject obj = new ReferObject("obj");
		PhantomReference<ReferObject> phantomReference = new PhantomReference<ReferObject>(obj, new ReferenceQueue<>()); 
		System.out.println(phantomReference.get());
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
		
		obj=null;
		//通知gc工作
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
		//通知gc工作
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
	}

	
}

 class ReferObject{
	private String id;
	//用来增大每个对象所占内存大小
	private double[] d = new double[30000]; 
	
	public ReferObject(String id) {
		this.id = id;
		System.out.println("create ReferObject "+id);
	}
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "object "+this.id;
	}
	
	@Override
	protected void finalize() throws Throwable {
		// TODO Auto-generated method stub
		System.out.println("finalize method executed for object "+this.id);
	}
}

这个例子可能有点长,博主会将它拆分开来讲,运行这个程序的虚拟机参数如下,用来限制堆内存大小:

java -Xmx1m -Xms1m

根据运行程序参数的不同 strong,soft,weak,phantom 分别代表着运行 strongReferenceTest(),softReferenceTest(),weakReferenceTest()和 phantomReferenceTest()这四个方法。下面博主就来详细解释下这四个方法和它们的运行结果。

public static void strongReferenceTest() {
		List<ReferObject> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			//实例化ReferObject
			ReferObject obj = new ReferObject(i.toString());
			//将对象放入list中防止被垃圾回收
			list.add(obj);
			System.out.println(obj);
		}
}

运行结果为:
create ReferObject 1
object 1
create ReferObject 2
object 2
create ReferObject 3
object 3
create ReferObject 4
object 4
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.wxueyuan.test.ReferObject.(ReferTest.java:142)
at com.wxueyuan.test.ReferTest.strongReferenceTest(ReferTest.java:36)
at com.wxueyuan.test.ReferTest.main(ReferTest.java:17)

很明显由于我们限制了虚拟机堆内存的大小为 1M,在我们建立了几个大的对象 ReferObject 之后,就堆内存溢出了。这个例子是想要告诉大家,我们通常情况下建立的对象都属于强引用,也就意味着虚拟机即使抛出内存异常也不会尝试去回收这些重要的对象。

public static void softReferenceTest() {
		SoftReference<ReferObject> sr = new SoftReference<ReferObject>(new ReferObject("obj"));
		System.out.println(sr.get());
		//通知jvm可以进行垃圾回收
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("after gc worked " +sr.get());
		System.out.println("***************************");
		
		List<SoftReference<ReferObject>> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			ReferObject obj = new ReferObject(i.toString());
			list.add(new SoftReference<ReferObject>(obj) );
			System.out.println(list.get(i-1).get());
			//每隔2s创建一个对象
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
}

运行结果为
create ReferObject obj
object obj
after gc worked object obj


create ReferObject 1
object 1
create ReferObject 2
object 2
finalize method executed for object obj
finalize method executed for object 2
finalize method executed for object 1
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.wxueyuan.test.ReferObject.(ReferTest.java:142)
at com.wxueyuan.test.ReferTest.softReferenceTest(ReferTest.java:60)
at com.wxueyuan.test.ReferTest.main(ReferTest.java:20)

我们先分析这个程序输出星号之前的部分,我们对一个 referObject 建立软引用,然后通过它的 get 方法获取了它引用的对象 obj,之后我们通知 GC 工作并等待了 2s,但事实上我们发现 Java GC 并没有回收掉这个对象,因为我们重写了对象的 finalize()方法,如果 GC 回收该对象,则 finalize()方法会被调用,此后我们再次调用 sr.get(),果然还能够获取到它引用的对象。

接下来我们看星号之后的部分,我们用 for 循环去大量创建 referObject,就在堆内存即将溢出之前,我们看到三个对象的 finalize()方法都被调用了,说明虚拟机会在内存溢出之前才尝试回收掉软引用引用的对象。

public static void weakReferenceTest() {
		WeakReference<ReferObject> wr = new WeakReference<ReferObject>(new ReferObject("obj"));
		//通知jvm可以进行垃圾回收
		System.out.println(wr.get());
		//等待gc工作
		System.gc();
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("after gc worked " +wr.get());
		System.out.println("***************************");
		
		List<WeakReference<ReferObject>> list = new ArrayList<>();
		for(Integer i =1; i<=10; i++) {
			ReferObject obj = new ReferObject(i.toString());
			list.add(new WeakReference<ReferObject>(obj) );
			
			//每隔2s创建一个对象
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(list.get(i-1).get());
		}
}

运行结果为:
create ReferObject obj
object obj
finalize method executed for object obj
after gc worked null


create ReferObject 1
object 1
create ReferObject 2
object 2
create ReferObject 3
finalize method executed for object 2
finalize method executed for object 1
object 3
finalize method executed for object 3
create ReferObject 4
object 4
create ReferObject 5
object 5
create ReferObject 6
finalize method executed for object 4
finalize method executed for object 5
object 6
create ReferObject 7
finalize method executed for object 6
object 7
create ReferObject 8
object 8
create ReferObject 9
finalize method executed for object 7
finalize method executed for object 8
object 9
finalize method executed for object 9
create ReferObject 10
object 10

同样我们也先分析这个程序输出星号之前的部分,我们首先建立 referObject 的弱引用,然后通过它的 get()方法获得它指向的对象,之后我们通知 GC 进行垃圾回收,我们可以看到 obj 对象的 finalize()方法被调用,同时弱引用的 get()方法无法再获得它原本指向的对象了。

接下来我们看星号之后的部分,我们用 for 循环去大量创建 referObject,但由于每个对象创建之间都间隔了两秒钟,因此 Java GC 在每次发现对象不可达之后就自动将对象所占的内存回收了,因此这个程序并不会内存溢出,同时我们也发现每个对象的 finalize()方法都被调用了,说明每个被弱引用指向的对象只能存活到下一次垃圾回收之前。

public static void phantomReferenceTest() {
		ReferObject obj = new ReferObject("obj");
		PhantomReference<ReferObject> phantomReference = new PhantomReference<ReferObject>(obj, new ReferenceQueue<>()); 
		System.out.println(phantomReference.get());
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
		
		obj=null;
		//通知gc工作
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
		//通知gc工作
		System.gc();
		//等待gc工作
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//查看对象是否不在内存中
		System.out.println(phantomReference.isEnqueued());
}

这个程序的运行结果如下:
create ReferObject obj
null
false
finalize method executed for object obj
false
true

虚引用的实例化需要两个参数,一个是引用的对象的实例,另一个就是 ReferenceQueue 的实例了,上面的代码首先通过虚引用的 get()方法获取引用实例,但是返回是 null,说明我们并不能通过虚引用获取对象实例。虚引用的 isEnqueued()方法可以用来告诉我们对象是否已经不在内存中,第一次查看时,对象实例并没有被 gc 回收因此返回 false;之后我们将 obj 指向 null,同时通知 GC 工作并等待 2s,我们发现对象的 finalize()方法执行了,但是虚引用的 isEnqueued()方法依旧返回 false,直到我们再次通知 GC 工作之后,虚引用的 isEnqueued()方法才返回 true,说明对象已经不再内存中了。这其实是因为对于重写了 finalize()方法的对象而言,GC 在发现该对象不可达后,会首先将它标记并放入 F-Queue 队列中,当 GC 再次工作时会将 F-Queue 中需要被回收的对象回收掉,因此在我们的例子中,第二次 System.gc()调用后,对象才真正被回收了。

3.死而复生的对象

利用重写 finalize()方法的特点,我们可以成功地将一个对象从被回收的边缘拯救回来,但是这种方法是及其不推荐的。在这里博主提供这个例子只是为了让大家更好地理解 GC 要回收一个对象时发生的事情。

public class ReviveObject
{
    public static ReviveObject obj=null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed for object ");
        //又将obj指向了当前对象实例
        obj = this;
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new ReviveObject();
        System.out.println(obj);
        obj = null; //将obj设为null
        //通知gc工作
        System.out.println("let GC do its work");
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
            System.out.println(obj);
        }

        obj = null;//由于obj被复活,此处再次将obj设为null
        System.out.println("let GC do its work again");
        System.gc();
        Thread.sleep(1000);
        if(obj == null) {
            //对象的finalize方法仅仅会被调用一次,所以当GC再次检测到对象不可达时,obj会直接被GC回收
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
        }
    }

}

运行结果大家应该也很好预料了:

com.wxueyuan.test.ReviveObject@15db9742
let GC do its work
finalize method executed for object
obj is alive
com.wxueyuan.test.ReviveObject@15db9742
let GC do its work again
obj is null

obj 对象的 finalize()方法在第一次 GC 工作时触发了,并且重新将 obj 指向了原来的实例,当第二次将 obj 指向 null 并通知 GC 工作后,GC 将直接回收掉已经触发过 finalize()方法的对象,因此第二次会发现 obj = null 了。

在本节博客中,博主与大家一起了解了两种判定对象是否应该回收的算法:引用计数法和可达性分析算法。两种算法各有好处,但是在 Java 中我们使用的时可达性分析算法。同时博主也提供了一个例子帮助大家了解 Java 中的四种引用类型及其特点。至于 finalize()方法,它的运行代价很高,不确定性大,它并不适合用来做释放资源的工作,博主在这里给出的例子只是为了方便大家理解 GC 工作时,finalize()方法被触发的条件仅此而已。如果大家对本篇博客中提到的 Reference 和 ReferenceQueue 感兴趣,欢迎大家观看博主的另一篇文章。那我们下篇博客见了~。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1083 引用 • 3461 回帖 • 262 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8208 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...