Java4种引用类型到底如何用?

JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。不同的引用在垃圾回收中体现也是不一样~

M

我们先创建一个M对象,后面为了方便的感受GC的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cyblogs.java.learning.C001_ReferenceType;

/**
* Created with java-learning-demo
*
* @description:
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:30 PM
*/
public class M {

@Override
public void finalize() {
System.out.println("finalize");
}

}

finalize函数是对象在gc的时候,一定会调用该方法。我们重写一下该方法并且打印一行日志。

强引用

只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.cyblogs.java.learning.C001_ReferenceType;

import java.io.IOException;

/**
* Created with java-learning-demo
*
* @description: 正常引用/强引用
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:31 PM
*/
public class C001_01_NormalReference {

public static void main(String[] args) throws IOException {
M m = new M();
// 将对象复制为空
m = null;
// 手动触发GC
System.gc();
// 因为不是触发gc就一定会立马gc,所以让线程阻塞一下
System.in.read();
}
}

控制台日志输出:

1
2
3
Connected to the target VM, address: '127.0.0.1:53621', transport: 'socket'
finalize
Disconnected from the target VM, address: '127.0.0.1:53621', transport: 'socket'

软引用

软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.SoftReference;

/**
* Created with java-learning-demo
*
* @description: 软引用:当内存不足的时候,gc才会回收。非常适合做缓存
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:31 PM
*/
public class C001_02_SoftReference {

public static void main(String[] args) throws InterruptedException {
// 开辟一个20M的空间
SoftReference<byte[]> m = new SoftReference<byte[]>(new byte[1024 * 1024 * 10]);
System.out.println(m.get());
// 手动GC一下,看是否可以GC掉
System.gc();
// 避免gc不会立马触发,尝试休眠1s
Thread.sleep(1000);
// 然后再尝试获取
System.out.println(m.get());
// 重新开辟一个空间
byte[] b = new byte[1024 * 1024 * 15];
System.out.println(m.get());
}
}

因为它是在内存不足的时候才会触发,所以我们在跑之前需要设置一下最大堆。

1
-Xmx20M

控制台日志输出:

1
2
3
4
5
Connected to the target VM, address: '127.0.0.1:54335', transport: 'socket'
[B@4c3e4790
[B@4c3e4790
null
Disconnected from the target VM, address: '127.0.0.1:54335', transport: 'socket'

你会发现就算我们gc了,后面还是会get得到,因为空间还足够。当后面byte[] b再继续申请空间的时候,发现空间不足了,这个时候就会触发gc动作,把软引用的部分清除掉。

弱引用

弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.WeakReference;

/**
* Created with java-learning-demo
*
* @description: 弱引用:是为了解决某些地方的内存泄露问题
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:32 PM
*/
public class C001_03_WeakReference {
public static void main(String[] args) {
WeakReference<M> m = new WeakReference<M>(new M());

System.out.println(m.get());
System.gc();
System.out.println(m.get());

ThreadLocal<M> tl = new ThreadLocal<M>();
tl.set(new M());
tl.remove();
}
}

控制台日志输出:

1
2
3
4
5
Connected to the target VM, address: '127.0.0.1:55151', transport: 'socket'
com.cyblogs.java.learning.C001_ReferenceType.M@38cccef
null
finalize
Disconnected from the target VM, address: '127.0.0.1:55151', transport: 'socket'

我们看一下ThreadLocalset方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

http://static.cyblogs.com/QQ20200616-225739@2x.jpg

为什么Entry要使用弱引用?

  • 若是强引用,即使tl=null,但是key的引用还是指向ThreadLocal。所以内存会泄露~而弱引用不会

  • 但是还会有内存泄露的问题,ThreadLocal被回收。key的值变成了null,则导致value的值再也无法被访问到,因此依然存在内存泄露问题。

虚引用

虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.cyblogs.java.learning.C001_ReferenceType;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

/**
* Created with java-learning-demo
*
* @description: 虚引用:
* @author: chenyuan
* @date: 2020/6/16
* @time: 9:33 PM
*/
public class C001_04_PhantomReference {

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


public static void main(String[] args) throws InterruptedException {
PhantomReference<M> m = new PhantomReference<M>(new M(), QUEUE);

// 线程1
new Thread(() -> {
while (true){
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
}
}).start();

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

Thread.sleep(500);
}
}

在零拷贝中就会使用到虚引用,但我们又无法去操作对外的内存。因为太弱了,我们也无法感知到~ 这里就需要利用到ReferenceQueue

http://static.cyblogs.com/QQ20200616-222909@2x.jpg

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号