六、强软弱虚四种引用以及ThreadLocal源码
强软弱虚引用
强引用
当我们使用Object obj = new Object()
创建一个对象时,指向这个对象的引用就称为强引用。只要这个引用还指向一个对象,那么指向的这个对象就不会被垃圾回收器回收。
package com.gouxiazhi.reference;
/**
* 强引用
* @author 赵帅
* @date 2021/1/17
*/
public class ReferenceDemo1 {
public static void main(String[] args) {
// 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M
// 先创建一个1M的字节数组,这是强引用
byte[] content = new byte[1024 * 1024];
// 再创建一个2M的字节数组,因为上面已经创建了1M了,强引用只要content还指向对象,就不会被垃圾回收器回收。因此会抛出内存溢出OOM异常。
// 当打开下面这句代码时,content不再指向一个对象,那么上面创建的对象就会被垃圾回收器回收,就可以正常运行了。
// content = null;
byte[] bytes = new byte[2 * 1024 * 1024];
}
}
软引用
使用软引用创建的对象,当内存不够时,就会被垃圾回收器回收。
package com.gouxiazhi.reference;
import java.lang.ref.SoftReference;
/**
* 软引用
* @author 赵帅
* @date 2021/1/17
*/
public class ReferenceDemo2 {
public static void main(String[] args) {
// 启动时设置jvm初始堆和最大堆大小为3M -Xms3M -Xmx3M
// 使用软引用创建一个1M的数组。
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024]);
System.out.println("reference = " + reference.get());
// 上面使用软引用创建了一个1M的数组,下面再创建2M的数组时,因为堆内存空间不够,就会调用gc清理掉软引用的指向的对象。因此不会抛出异常。
// 如果上面不使用软引用,而使用 byte[] a = new byte[1024*1024];就会抛出内存溢出异常。
byte[] bytes = new byte[2 * 1024 * 1024];
// 因为创建上面的对象内存不够,因此软引用指向的对象已经被回收
System.out.println("reference = " + reference.get());
}
}
弱引用
使用弱引用创建的对象,只要垃圾回收器看见,就会回收。
package com.gouxiazhi.reference;
import java.lang.ref.WeakReference;
import java.util.Arrays;
/**
* @author 赵帅
* @date 2021/1/17
*/
public class ReferenceDemo3 {
public static void main(String[] args) {
// 弱引用指向的对象,只要垃圾回收器看见就立马回收掉。
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024]);
System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
System.gc();
System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
}
}
虚引用
主要用来管理堆外内存等。
package com.gouxiazhi.reference;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* @author 赵帅
* @date 2021/1/17
*/
public class ReferenceDemo4 {
public static class M{
@Override
protected void finalize() throws Throwable {
System.out.println("对象被回收了");
super.finalize();
}
}
public static void main(String[] args) {
ReferenceQueue<M> referenceQueue = new ReferenceQueue<>();
// 虚引用的使用,除了指向一个对象,还需要指定一个引用队列。
PhantomReference<M> reference = new PhantomReference<>(new M(), referenceQueue);
System.out.println("reference = " + reference);
// 虚引用指向的对象无法被获取到,弱引用被垃圾回收器看见就会被回收,虚引用比弱引用级别更低
System.out.println("reference = " + reference.get()); // null
// 虚引用一般都是指向一个堆外内存,因为垃圾回收器只能回收堆内存,无法管理堆外内存.
// 如果使用java管理堆外内存。假设M代表着堆外内存,那么当虚引用被回收时,他会将自身放入referenceQueue引用队列,开启另一个线程监听这个队列,
// 当这个队列取到内容时,就代表要回收这块堆外内存了,可以执行回收堆外内存操作
new Thread(() -> {
// 如果从队列中取到虚引用,那么就表示需要回收这个堆外内存了。
Reference<? extends M> poll = referenceQueue.poll();
if (poll != null) {
System.out.println("虚引用对象被jvm回收了" + poll);
}
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[4 * 1024]);
}
}
}
ThreadLocal源码
查看ThreadLocal
的set方法源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
可以看到,在set方法中,首先获取当前线程。然后通过getMap(t)
获取了一个ThreadLocalMap对象。然后将要保存的对象存入了这个Map中,key值就是ThreadLoacl对象本身,value值为要保存的数据。
然后我们再点开getMap
方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
返回的是t.threadLocals
属性值,而且这个值是ThreadLocalMap
类型的。查看ThreadLocalMap
类:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到ThreadLocalMap
中存放数据的Entry
节点继承自WeakReference<?>
,因此说ThreadLocal
底层用的是弱引用,而且在存储时,Map的key作为弱引用,也就是ThreadLocal对象本身作为弱引用存放,值是强引用存放的。
查看ThreadLocalMap类的set方法:
private void set(ThreadLocal<?> key, Object value) {
// 获取存放数据的数组,底层数据结构
Entry[] tab = table;
// 获取数组的长度
int len = tab.length;
// 计算key要存放的下标
int i = key.threadLocalHashCode & (len-1);
// 从下标i开始遍历数组,如果下标i 有元素了,也就是hash冲突了,那么就往后插并将下标自增
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取下标为i的entry元素
ThreadLocal<?> k = e.get();
// 如果此位置的key与要保存的key相同则替换值
if (k == key) {
e.value = value;
return;
}
// 如果key是空的话,说明这个位置的key已经过期被回收,则替换值为新的值。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果下标i为空,说明这个位置是空的。插入这个位置
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
简单分析上面整个过程,整个threadlocal存放数据过程甬道图如下:
![image-20210118143336157](/Users/zhaoshuai/Library/Application Support/typora-user-images/image-20210118143336157.png)
当调用threadLocal的set方法时:
- 获取当前线程的threadLocals属性。
- 调用threadLocals属性的set方法。
- 获取threadLocalMap的entry数组。
- 计算当前threadLocal对象的下标。
- 获取下标的entry,如果没有则新建一个entry,如果有的话,判断当前的key是否与要存入的key一致,如果一致,则替换值。如果key为空的话,则替换值。如果上面条件都不满足,则新建一个entry对象。
ThreadLocal造成的内存泄漏
因为ThreadLocal是将自身作为弱引用存放在ThreadLocalMap中的,因此当一个ThreadLocal对象的强引用消失时。那么这个key将会被回收,这时原来这个key对应的value值如果没有被移除的话,那么就永远无法被访问到了。而且因为这个value值是作为entry节点的value引用指向的,value引用是一个强引用,那么这时,这个value属性就永远无法被回收,也无法被访问,就会造成内存泄漏。
使用ThreadLocal如何避免内存泄漏?
使用ThreadLocal set了一个值以后,在这个线程结束之前,一定要调用remove方法移除存放的值。