1理解Java的强引用、软引用、弱引用和虚引用
1强引用(StrongReference)
强引用是使用最普遍的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:
strongReference = null;
方法的内部使用强引用:这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。
strongReference是全局变量时:就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
public class TO1_NormalReference{
public static void main(String[] args) throws IOException {
M m= new M();
m = null;
System.gc(); //DisableExplicitGC
//System. out. printIn(m) ;
System. in.read();//阻塞main线程,给垃圾回收线程时间执行
}
}
2软引用(SoftReference)
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。
public class TO2_ _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( millis: 500) ;
} catch (Inter ruptedException e) {
e.printStackTrace() ;
}
System. out . println(m.get()); // 没有被GC回收
//再分配一个数组, heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够, 会把软引用回收。
byte[] b = new byte [1024* 1024*15] ;
System.out . println(m.get());
// 软引用可用来实现内存敏感的高速缓存。
}
}
3弱引用(WeakReference)
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
public class TO3_ WeakReference {
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());
/*ThreodLocal<M> tl = new ThreadLocal<>();
tl.set(new M());
tl. remove();*/
}
}
4虚引用(PhantomReference)
虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,管理直接内存使用的。
public class TO4_ PhantomReference {
private static final List<object> LIST = new LinkedList<>();
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<M> phantomReference = new PhantomReference<> (new M(), QUEUE) ;
new Thread(() -> {
while (true) {
LIST.add(new byte[1024★1024]);
try{
Thread.sleep( millis: 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( millis: 500);
} catch (InterruptedException e) {
e.printStackTrace() ;
}
}
}
2 理解ThreadLocal
package com.zs.thread;
public class WriteReadTest {
//线程本地存储变量
private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {//启动三个线程
Thread t = new Thread(() -> add10ByThreadLocal());
t.start();
}
}
/**
* 线程本地存储变量加 5
*/
private static void add10ByThreadLocal() {
for (int i = 0; i < 5; i++) {
Integer n = THREAD_LOCAL_NUM.get();
n += 1;
THREAD_LOCAL_NUM.set(n);
System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
}
}
}
Thread类声明了成员变量threadLocals,threadLocals才是真正的线程本地变量,因此每个 Thread 都有自己的线程本地变量,所以线程本地变量拥有线程隔离特性,也就是天生的线程安全。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
threadLocals 成员变量类是 ThreadLocal.ThreadLocalMap,即是 ThreadLocal 提供的内部类,因此 Thread 线程本地变量的创建、新增、获取、删除实现核心,必然是围绕 threadLocals,所以开发者也是围绕 threadLocals 实现功能,为了后续重复使用,还会对代码实现进行封装复用,而 ThreadLocal 就是线程本地变量工具类,由 J D K 提供,线程本地变量的功能都已经实现好了,开箱即用。
一个 Thread可以拥有多个 ThreadLocal键值对(存储在ThreadLocalMap结构),又因为 ThreadLocalMap 依赖当前Thread,Thread销毁时 ThreadLocalMap 也会随之销毁,所以 ThreadLocalMap 的生命周期与 Thread 绑定。
「本地线程变量的作用域,属于当前线程整个范围,一个线程可以跨越多个方法使用本地线程变量」,当你希望某些变量在某 Thread 的多个方法中共享并保证线程安全,那就大胆的使用ThreadLocal。
2.1 ThreadLocal源码
package com.zs.thread;
class ThreadLocalTest {
private ThreadLocal<ThreadVariable> threadLocal = new ThreadLocal<ThreadVariable>(){
@Override
protected ThreadVariable initialValue() {
return new ThreadVariable("threadVariable initialValue()");
}
};
public ThreadVariable getThreadVariable() {
return threadLocal.get();
}
public void setThreadLocalVariable(ThreadVariable threadLocalVariable) {
threadLocal.set(threadLocalVariable);
}
public void clear() {
threadLocal.remove();
}
}
class ThreadVariable {
private String name;
public ThreadVariable(String name) {
this.name = name;
}
}
2.2 ThreadLocalMap结构
ThreadLocalMap 结构已经非常清晰,发现 ThreadLocal 竟被弱引用持有?为什么ThreadLocal会被弱引用?
2.2.1 get 获取变量
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程本地变量
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果线程本地变量不为空,通过当前ThreadLocal作为索引获取对应的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果Entry对象不为空,获取value值并返回
T result = (T)e.value;
return result;
}
}
/如果本地线程变量为null或者value为空,执行初始化value
return setInitialValue();
}
步骤如下:
1、获取当前线程
2、获取当前线程的本地变量
3、线程本地变量没有被创建,执行setInitialValue方法进行初始化,并返回value值
4、线程本地变量存在,ThreadLocal计算成索引从 本地线程变量 获取Entry,如果Entry为null,执行setInitialValue方法进行初始化,并返回value值,否则通过Entry获取value返回
2.2.2 initialValue方法
private T setInitialValue() {
// 执行initialValue方法,获取value
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程本地变量
ThreadLocalMap map = getMap(t);
// 线程本地变量不为空,当前ThreadLocal作为索引设置映射的value
if (map != null)
map.set(this, value);
else
// 如果线程本地变量为空,创建线程本地变量,并把当前ThreadLocal作为索引设置映射的value
createMap(t, value);
return value;
}
步骤如下:
通过get方法触发
执行初始化,获取到value
获取当前线程
获取当前线程本地变量
如果当前线程本地变量存在 ,ThreadLocal计算成索引设置映射的value,否则创建线程本地变量再做后续的设置操作
返回value值
2.2.3 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);
}
步骤如下:
获取当前线程
获取线程本地变量
本地变量不为空,当前ThreadLocal为索引设置映射的value,否则创建线程本地变量再做后续的设置操作
2.2.4 remove 清除变量
private void remove(ThreadLocal<?> key) {
/获取Entry数组
Entry[] tab = table;
int len = tab.length;
// 计算出当前ThreadLocal的数组下标
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//遍历,直到找到Entry中key为当前对象key的那个元素
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
步骤如下:
获取Entry数组
当前ThreadLocal计算出索引
根据索引获取Entry元素(若是第一次没有命中,就循环直到null)
清除Entry元素
核心就三样:ThreadLocal线程本地变量工具类(同时作为索引)、Entry基本元素(由弱引用包装类ThreadLocal与value组成),Entry数组容器,到这里流程很清晰了。ThreadLocal计算出数组索引,用 ThreadLocal 与 value 构建出 Entry 元素,最终放入 Entry 容器中。
Entry的key都是一个弱引用:如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用)。
理解ThreadLocal中的内存泄漏问题
虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候(tl = null)
,就会自动被回收,但是Entry中的value依然是强引用。
只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理。
ThreadLocal并不能100%保证不发生内存泄漏,如果get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行;如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。