java中的reference(一): GC与4种基本的Reference(强软弱虚)

时间:2022-07-22
本文章向大家介绍java中的reference(一): GC与4种基本的Reference(强软弱虚),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

文章目录

1. java对象分配和gc的简单介绍

在Java中,一切对象都只能通过new进行实例化。在执行了new关键字之后,jvm将在堆上分配java对象。而java的局部变量,则被放到了栈上。(在栈上的只是一个指向堆上面这个对象地址的指针。不是这个对象本身。)有如下方法:

	public static void foo(String bar) {
		Integer baz = new Integer(bar);
	}

这个方法分配了一个Integer对象,而且解析一个传入的字符串bar来获得最终的值。在栈中,栈被分成了很多个帧,而每个帧则是由调用树上的方法参数和其局部变量组成。 上面这个例子里,参数bar和局部变量都指向了位于堆中的变量。如下图:

在foo函数中,分配了一个Integer对象,大约占16字节。 参考 https://blog.csdn.net/wuwenxiang91322/article/details/80216420?utm_source=blogxgwz1

public static void main(String[] args) {
		System.out.println("Integer: " + ObjectSizeCalculator.getObjectSize(Integer.valueOf(122)));
	}

执行结果:

Integer: 16

那么jvm就会再堆内存中分配一块16字节的空间,之后调用构造函数初始化这个对象。之后将这个对象的指针存到变量表biz中。如果jvm中没有找到足够可分配的空间, 那么就会调用GC。 对于java而言,通过new可以分配空间,但是并没有像c++析构函数那样用以显示回收内存的方法让你在使用完成之后对内存进行释放。java都通过GC自动进行回收清理。当程序尝试分配一个新对象,但是又没有足够的内存进行分配的时候,就会触发GC。(当然,导致GC的产生会有很多种可能)。如果还是不能分配,则会触发OutOfMemoryError错误。 java在进行GC的时候,会通过可达性算法,从根对象开始,遍历所有的对象图,并将访问过的对象都打上标记。(三色标记算法,后面会用专门的文章对GC进行详细的讨论)。之后将需要回收的对象进行清理。最后再将清理之后的内存空间进行整理。当然,不同的垃圾回收器会使用不同的回收算法,回收过程也不同。 那么我们需要关注的是,什么情况下,内存会被回收呢?在不同的场景下,回收的处理是相同的吗?这就是本文需要讨论的重点,java中的Reference。

2.四种基本的Reference

为了理解java中的Reference,那么我们先来看一个例子。参考https://dzone.com/articles/java-garbage-collector-and-reference-objects 假定有一家自助餐厅。那么餐厅最稀缺的资源就是盘子。顾客用餐必须使用盘子,如果要提高餐厅的效益,必须让每个顾客都有盘子。但是我们不能将盘子买得太多,那样会增加很多不必要的成本。因此你必须定期将顾客吃完之后的盘子进行回收,清洗,以供其他顾客重复使用。采用这个策略之后,一切都安好。但是你会发现,有很多顾客已经就餐完成,但是在餐桌上闲聊而占用了不必要的资源。在就餐的高峰期,还是有很多人没有盘子不得不排队或者造成顾客流失去。 对于这种情况,你制定了如下策略。只需要顾客就餐完毕,服务员就可以将盘子回收。这样以就餐的顾客就会离开。但是,对于某些VIP客户,为了保证服务质量,你还是需要等到对方离开之后。有如下策略:

  • 高级VIP需要等到自行离去之后才能回收盘子。因为高级VIP提供了更高的服务费。
  • 对于一般的VIP,如果在盘子不够用的情况下,也可以强行回收,只要对方就餐完毕即可。不用等到对方离开。
  • 对于普通的就餐人员,只需要就餐完毕就会回收盘子。
  • 对于特殊的就餐人员,如糖尿病患者,可以回收盘子,但是需要将回收时间进行记录,以便医生能跟踪其就餐的情况。

这能很好的解决餐厅就餐的问题。言归正传。jvm内存回收也一样。正如文中的盘子一样,jvm的内存回收,也有很多类型的场景需要讨论。

2.1 Strong Reference (强引用)

如前文所述,在java中,通过等号“=”就建立了一个强的引用。

	public static void main(String[] args) {
		Object obj = new Object();
		System.out.println(obj);
		System.gc();
		System.out.println(obj);
		obj = null;
		System.out.println(obj);
	}

这正如上文中的VIP,除非显示的将null赋值给这个变量,那么GC默认是不会对此进行回收的。只有在null进行赋值之后,那么obj就会被回收。 这也是java种创建对象的默认的引用方式。如果我们要让GC用其他的方式处理,那么你需要使用java.lang.ref包。

2.2 Soft Reference 软引用

通过java.lang.ref.SoftReference能够创建一个软引用。

	public static void main(String[] args) {
   	SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
   	System.out.println(m.get());
   	System.gc();
   	try {
   		Thread.sleep(500);
   	}catch (InterruptedException e){
   		e.printStackTrace();
   	}
   	System.out.println(m.get());

   	byte[] b = new byte[1024*1024*15];
   	System.out.println(m.get());

   }

其过程如上代码。通过SoftReference能够创建一个对象的软引用。 我们将jvm参数设置小一点,配置为:

-Xms30m -Xmx30m  -XX:+PrintGCDetails -XX:+UseParNewGC -XX:+UseCompressedOops -XX:+PrintReferenceGC

-Xms30m -Xmx30m -XX:+PrintReferenceGC

其执行结果如下:

"D:Program FilesJavajdk1.8.0_91binjava.exe" -Xms30m -Xmx30m -XX:+PrintGCDetails -XX:+UseParNewGC -XX:+UseCompressedOops -XX:+PrintReferenceGC " ...
[B@14ae5a5
[Full GC (System.gc()) [Tenured[SoftReference, 0 refs, 0.0000190 secs][WeakReference, 4 refs, 0.0000057 secs][FinalReference, 25 refs, 0.0000082 secs][PhantomReference, 0 refs, 0 refs, 0.0000067 secs][JNI Weak Reference, 0.0000036 secs]: 10240K->10896K(20480K), 0.0027029 secs] 12406K->10896K(29696K), [Metaspace: 3278K->3278K(1056768K)], 0.0027343 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[B@14ae5a5
[GC (Allocation Failure) [ParNew[SoftReference, 0 refs, 0.0000144 secs][WeakReference, 0 refs, 0.0000057 secs][FinalReference, 0 refs, 0.0000062 secs][PhantomReference, 0 refs, 0 refs, 0.0000093 secs][JNI Weak Reference, 0.0000026 secs]: 163K->0K(9216K), 0.0003789 secs][Tenured[SoftReference, 0 refs, 0.0000129 secs][WeakReference, 0 refs, 0.0000051 secs][FinalReference, 5 refs, 0.0000062 secs][PhantomReference, 0 refs, 0 refs, 0.0000062 secs][JNI Weak Reference, 0.0000026 secs]: 10896K->10896K(20480K), 0.0025091 secs] 11060K->10896K(29696K), [Metaspace: 3278K->3278K(1056768K)], 0.0029306 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured[SoftReference, 29 refs, 0.0000149 secs][WeakReference, 0 refs, 0.0000046 secs][FinalReference, 2 refs, 0.0000041 secs][PhantomReference, 0 refs, 0 refs, 0.0000057 secs][JNI Weak Reference, 0.0000026 secs]: 10896K->633K(20480K), 0.0026628 secs] 10896K->633K(29696K), [Metaspace: 3278K->3278K(1056768K)], 0.0026952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
Heap
par new generation   total 9216K, used 246K [0x00000000fe200000, 0x00000000fec00000, 0x00000000fec00000)
 eden space 8192K,   3% used [0x00000000fe200000, 0x00000000fe23d8f0, 0x00000000fea00000)
 from space 1024K,   0% used [0x00000000feb00000, 0x00000000feb00000, 0x00000000fec00000)
 to   space 1024K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000feb00000)
tenured generation   total 20480K, used 15993K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
  the space 20480K,  78% used [0x00000000fec00000, 0x00000000ffb9e510, 0x00000000ffb9e600, 0x0000000100000000)
Metaspace       used 3285K, capacity 4496K, committed 4864K, reserved 1056768K
 class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

可以看到,手动执行GC的时候,软引用的对象并没有回收,因为此时内存大小还能满足。接下拉再次申请一个对象分配了15M内存,由于内存不够,触发了GC。那么软引用此时已经被回收了。第三次输出的时候为null。 文中采用的CMS垃圾回收器,如果用G1也会产生相同的效果。 对于jvm只有当且仅当内存不够将要出现OOM异常的时候,才会对软引用进行回收。这也类似于前文中的餐厅里的普通VPI用户,没有足够的盘子的时候,就会对这些用户进行回收。

2.3 Weak Reference 弱引用

弱引用可以通过java.lang.ref.WeakReference进行实现。弱引用一旦对该对象的强引用消失,GC的时候就会立即回收。

	public static void main(String[] args) {
		WeakReference<M> m = new WeakReference<>(new M());
		System.out.println(m.get());
		System.gc();
		System.out.println(m.get());

	}

其执行结果如下:

com.dhb.test.M@14ae5a5
null

如果这个弱引用的对象被强应用持有,则不会被回收。弱引用的使用场景非常适合缓存,再就是还有一个重要的应用就是ThreadLocal,那么我们将在后面单独对threadLocal进行讨论。 如果改成如下代码:

	public static void main(String[] args) {
		WeakReference<M> m = new WeakReference<>(new M());
		System.out.println(m.get());
		M t = m.get();
		System.gc();
		System.out.println(m.get());
		t = null;
		System.gc();
		System.out.println(m.get());
		
	}

执行结果如下:

com.dhb.test.M@14ae5a5
com.dhb.test.M@14ae5a5
null

第二次打印的过程中,由于存在强引用,则GC不会回收。在显示将t=null之后,这个弱引用立即会被回收。这也是ThreadLocal种最常用的场景。

这类似于前文餐厅种的普通就餐人员,只要就餐完毕,那么立即回收他们的盘子。

2.4 Phantom Reference 虚引用

通过java.lang.ref.PhantomReference实现的虚引用,需要配合ReferenceQueue 一起才能使用。 在其源码的get方法中:

 public T get() {
        return null;
    }

直接返回了null。 对虚引用进行测试:

public class TestPhantomRefrence {

	public static final List<Object> LIST = new LinkedList<>();
	public static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();


	public static void main(String[] args) {
		PhantomReference<M> phantomReference = new PhantomReference<>(new M(),QUEUE);
		System.out.println(phantomReference.get());

		new Thread(() -> {
			while (true) {
				LIST.add(new byte[1024*1024]);
				try {
					Thread.sleep(1000);
				}catch (InterruptedException e) {
					e.printStackTrace();
					Thread.currentThread().interrupt();
				}
				System.out.println(phantomReference.get());
			}
		}).start();

		new Thread(() -> {
			while (true) {
				Reference<? extends M> poll = QUEUE.poll();
				if(poll != null) {
					System.out.println("----虚引用被jvm回收了---"+poll);
				}
			}
		}).start();

		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

在此,我们构造了两个线程,一个线程每隔1秒,持续给一个虚引用的Queue中添加元素。另外一个线程对ReferenceQueue进行轮询,对其回收的情况进行监控。将jvm设置如下:

 -Xms30m -Xmx30m  -XX:+PrintGCDetails -XX:+UseParNewGC -XX:+UseCompressedOops -XX:+PrintReferenceGC

那么其执行结果为:

"D:Program FilesJavajdk1.8.0_91binjava.exe" -Xms30m -Xmx30m -XX:+PrintGCDetails -XX:+UseParNewGC -XX:+UseCompressedOops -XX:+PrintReferenceGC " ...
null
null
null
[GC (Allocation Failure) [ParNew[SoftReference, 0 refs, 0.0000278 secs][WeakReference, 130 refs, 0.0000103 secs][FinalReference, 0 refs, 0.0000031 secs][PhantomReference, 1 refs, 0 refs, 0.0000067 secs][JNI Weak Reference, 0.0000077 secs]: 7588K->1024K(9216K), 0.0016517 secs] 7588K->3336K(29696K), 0.0016702 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
----虚引用被jvm回收了---java.lang.ref.PhantomReference@1f64aa1d
null
null
null
null

我们可以在ReferenceQueue中收到其被回收的消息。 当然这个程序由于这个引用被回收,后面thread持续运行会出错。 这就好比在餐厅的糖尿病患者,我们需要对其就餐的情况进行记录,但是可以一上来就回收他们的盘子。 虚引用主要适用于对外内存的分配场景。如ByteBuffer.allocateDirect()方法或者使用DirectByteBuffer类。在引用消失之后,我们需要对其进行记录到ReferenceQueue,在GC的时候jvm通过对ReferenceQueue进行检查,之后将对外的内存进行回收。