logo头像
Snippet 博客主题

浅谈Java对象四大引用

1. Java四大对象引用

在jdk1.2之前,Java引用通俗意义上来讲就是对象引用和被引用的关系,实际定义为:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

在JDK1.2之后,Java对引用的概念将引用扩充为强引用(StronglyReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱。

1.1 强引用(StronglyReference)

1.1.1 概念定义

  • Object obj=new Object()这种引用赋值的关系就是强引用。
  • 只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

1.1.2 代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @desc M实体
* @author LT
* @date 2020-03-15 20:34:17
*/
public class M {

@Override
protected void finalize() throws Throwable {
super.finalize();
// 这里只是演示,生产环境禁止重写改方法
System.out.println("---------作为M的我,被回收了-------------------" + this);
}
}
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
/**
* @desc Java4大对象引用
* @author LT
* @date 2020-03-15 20:45:07
*/
public class ReferenceTest {

/**
* 强引用:
* 直接new出来的对象直接赋值给引用
* 当引用关系不存在的时,对象回收
*/
@Test
public void stronglyReference() {
// 在堆里面new对象
M m = new M();
// 引用还在
System.out.println(m);
// 手动将引用设置为null
m = null;
// 手动GC回收,M复写的finalize方法会调用,此时m对象会回收
System.gc();
// 这个时候就获取到m就为null
System.out.println(m);
}
}
1
2
3
4
# 执行结果
com.java.entity.M@4ee285c6
---------作为M的我,被回收了-------------------com.java.entity.M@4ee285c6
null

1.2 软引用(SoftReference)

1.2.1 概念定义

  • 软引用是用来描述一些还有用,但非必须的对象,典型案例就是缓存的应用。
  • 只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 在JDK 1.2版之后提供了SoftReference类来实现软引用。

1.2.2 代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @desc M实体
* @author LT
* @date 2020-03-15 20:34:17
*/
public class M {

// 类加载时申请10M空间
private static final byte[] b = new byte[1024 * 1024 * 10];

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("---------作为M的我,被回收了-------------------" + this);
}

}
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
/**
* 软引用:
* 被SoftReference类引用的对象,只有在空间不足的情况下进行GC回收。
* 很适合做缓存。
*/
@Test
public void softReference() {
// 申请10M的空间,并给软引用
SoftReference<M> m = new SoftReference<>(new M());
// 可以打印出字节数组对象
System.out.println(m.get());
System.gc();
try {
// 睡眠500毫秒,等待GC回收
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 继续打印
System.out.println(m.get());
// 再分配一个15M字节数组,heap不够,这个时候系统会垃圾回收,先回收一次,如果不够,会把软引用的对象回收掉
byte[] b = new byte[1024 * 1024 * 15];
// 继续打印
System.out.println(m.get());
}
1
2
3
4
5
6
7
8
9
10
11
# 直接运行都可以打印出相同的对象
com.java.entity.M@4ee285c6
com.java.entity.M@4ee285c6
com.java.entity.M@4ee285c6
# 当在启动时候加上JVM参数-Xmx20M时,会回收报堆内存不足,此时会回收软引用的对象
com.java.entity.M@4ee285c6
com.java.entity.M@4ee285c6
---------作为M的我,被回收了-------------------com.java.entity.M@4ee285c6

java.lang.OutOfMemoryError: Java heap space
at com.java.reference.ReferenceTest.softReference(ReferenceTest.java:57)

1.3 弱引用(WeakReference)

1.3.1 概念定义

  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
  • 当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 在JDK 1.2版之后提供了WeakReference类来实现弱引用。

1.3.2 代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 弱引用:
* 被WeakReference类引用的对象,只要有GC都回收该对象。
* ThreadLocal就是典型的应用
*/
@Test
public void weakReference() {
// 创建虚弱引用对象,并指向M对象
WeakReference<M> m = new WeakReference<>(new M());
// 可以获取到m对象
System.out.println(m.get());
// GC垃圾回收
System.gc();
// 已经无法获取到m对象
System.out.println(m.get());
}
1
2
3
4
# 运行结果
com.java.entity.M@4ee285c6
null
---------作为M的我,被回收了-------------------com.java.entity.M@4ee285c6

1.4 虚引用(PhantomReference)

1.4.1 概念定义

  • 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。
  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
  • 为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
  • 在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

1.4.2 代码演示

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package com.java.reference;

import com.java.entity.M;
import org.junit.Test;
import java.lang.ref.*;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
* @desc Java4对象大引用
* @author LT
* @date 2020-03-15 20:45:07
*/
public class ReferenceTest {

/**
* 对象列表
*/
public static final List<Object> LIST = new LinkedList<>();

/**
* 引用队列
*/
public static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

/**
* 虚引用:
* 被PhantomReference类引用的对象,堆外内存的回收。
* JVM进行垃圾回收的时候,不会立即回收该对象引用,而会把该对象的引用放入队列中等待其他线程处理
* Netty中DirectBuffer内存区就属于堆外内存。
*/
@Test
public void phantomReference() {
// 创建虚引用对象,并指定存放M对象,将回收引用放入QUEUE
PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);
// 创建线程池,核心线程数2个
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 第一个用于存储对象
threadPool.execute(() -> {
while (true) {
LIST.add(new byte[1024 * 1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
// 这里永远get到的结果为null,虚引用在堆内存就是没有意义
System.out.println(phantomReference.get());
}
});

/**
* 队列检测通知
*/
threadPool.execute(() -> {
while (true) {
// 获取队列的引用
Reference<? extends M> poll = QUEUE.poll();
if (poll != null) {
// 引用回收通知
System.out.println("-------虚引用对象被回收了-------------" + poll);
}
}
});

try {
// 等待GC回收
threadPool.awaitTermination(1,TimeUnit.DAYS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 这里设置一下-Xmx20M,不然一直创建对象内存会爆掉,这里设置堆大小跟虚引用没任何关系,只是为了演示效果
# 应用刚启动,就回收了虚引用对象,所以说虚引用创建起来并没有什么实际用处,一直get为null,只是为了回收通知
---------作为M的我,被回收了-------------------com.java.entity.M@32a48caa
null
null
null
null
null
null
-------虚引用对象被回收了-------------java.lang.ref.PhantomReference@113fd376
null
Exception in thread "pool-1-thread-1" java.lang.OutOfMemoryError: Java heap space
at com.java.reference.ReferenceTest.lambda$phantomReference$0(ReferenceTest.java:111)
at com.java.reference.ReferenceTest$$Lambda$1/1562557367.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

2. 引用的应用剖析

2.1 ThreadLocal中的弱引用应用

  1. 熟悉ThreadLocal源码

    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
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    public class ThreadLocal<T> {

    public T get() {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取当前线程threadLocals变量,其实就是Thread的成员变量
    if (map != null) { // 如果获取到map
    ThreadLocalMap.Entry e = map.getEntry(this); // 通过当前TheadLocal对象作为key获取value
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value; // 泛型检查
    return result; // 返回vaue
    }
    }
    return setInitialValue();
    }

    public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程对象
    ThreadLocalMap map = getMap(t); // 获取当前线程threadLocals变量
    if (map != null) // 如果不为空,将当前ThreadLocal作为key,组成ThreadLocalMap.Entry对象
    map.set(this, value);
    else
    createMap(t, value); // 创建线程ThreadLocalMap对象,并存放ThreadLocal和value
    }

    public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程对象
    if (m != null)
    m.remove(this); // 根据当前ThreadLocal对象移除对象
    }

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; // 从线程中获取threadLocals变量
    }

    void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue); // 直接创建一个本地的ThreadLocalMap
    }

    static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> { // 这里采用弱引用
    /** The value associated with this ThreadLocal. */
    Object value; // 这个value和当前ThreadLocal关联

    Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
    }

    }

    private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
    return e;
    else
    return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
    ThreadLocal<?> k = e.get();
    if (k == key)
    return e;
    if (k == null)
    expungeStaleEntry(i);
    else
    i = nextIndex(i, len);
    e = tab[i];
    }
    return null;
    }

    private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null 直到我们null再rehash一次
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
    (e = tab[i]) != null;
    i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == null) { // 找到ThreadLocal指向为null的key
    e.value = null; // 当key引用为null,对象已回收,释放空间,一定程度上解决内存泄露
    tab[i] = null;
    size--;
    } else {
    int h = k.threadLocalHashCode & (len - 1);
    if (h != i) {
    tab[i] = null;
    // Unlike Knuth 6.4 Algorithm R, we must scan until
    // null because multiple entries could have been stale.
    while (tab[h] != null)
    h = nextIndex(h, len);
    tab[h] = e;
    }
    }
    }
    return i;
    }

    private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
    e != null;
    e = tab[i = nextIndex(i, len)]) {
    if (e.get() == key) { // 假设有一个map中有一个e.get()和key永远不等,因为一个ThreadLocal的生命周期和对应的Thread周期是一致的,线程结束掉,Thread就会死掉,Thread中的成员变量threadLocas就获取不到,ThreadLocal对象也会回收,所以会存在内存泄露
    e.clear(); // 移除当前线程变量数据
    expungeStaleEntry(i);
    return;
    }
    }
    }

    }

    }
  1. ThreadLocal分析与总结

    为什么ThreadLocal中的ThreadLocalMap.Entry要使用弱引用?

    • 若是强引用,即使threadLocal对象强制设置为null,但key的引用依然指向ThreadLocal对象,所以会有内存泄露,而使用弱引用则不会。
    • 但是还有内存泄露存在的可能,ThreadLocal被回收,key的值变为null,则导致整个value再也无法被访问到,因此依然存在内存泄露。
    • 为了使ThreadLocal对象回收,我们尽可能不要定义static的ThreadLocal对象,不要延长它的生命周期。另外,尽可能手动调用remove方法提前通知JVM回收

3. 参考文档

支付宝打赏 微信打赏

请作者喝杯咖啡吧