1 强引用
1.1 什么是强引用
// 情况1
static Object o = new Object();
// 情况2
public void get(){
Object o = new Object();
}
就是最普通的创建对象方式,当使用new关键字经过创建对象的三个步骤完成之后,对象o此时就有了一个指向堆内存中这个对象的一个引用。
1.2 与垃圾回收的关系
1.2.1 什么可以作为GC Root
由于Java使用的垃圾回收机制是根可达算法,使用为GC Root的对象都有以下几种
-
虚拟机栈(栈帧中的本地变量表)中引用的对象。
-
本地方法栈中JNI(即一般说的native方法)引用的对象。
-
方法区中的静态变量和常量引用的对象。
1.2.2 何时回收
-
如果在线程执行方法的的过程中创建的o对象,在此方法在仍然在执行过程中时(也就是这个方法的栈帧仍然存在于在线程栈中时)创建的变量,只要不显示声明为
o = null
,此对象会一直存在于堆中,即使发生GC,也不会回收。 -
执行完成此方法,栈帧弹出,o自动消失,也会发生不可达情况,在发生GC的时候被回收。
-
静态变量在类被卸载的时候才会不再可达,才会在GC中回收
public class NormalReference {
public static void main(String[] args) throws IOException {
MyObject o = new MyObject();
o = null;
System.gc(); // Full GC
}
}
public class MyObject {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
2 软引用
2.1 什么是软引用
简单说就是当创建对象需要的空间大小不够的时候,会触发一次GC,如果GC完成之后仍然不够此对象分配,则回收掉软引用指向的对象(而不是回收软引用本身)。
2.2 代码实验
// VM -Xms20M -Xmx20M
public class SoftReference {
public static void main(String[] args) {
//此时的对象指引关系如上图所示(上图并不规范,讲道理o应该在main线程栈空间中,而softreference也应该位于堆空间中,理解意思就行哈)
SoftReference<byte[]> o = new SoftReference<>(new byte[1024*1024*10]);
System.out.println(o.get());
// Full GC 之后仍可获得软引用指向的对象
System.gc();
System.out.println(o.get());
//再分配一个数组,heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够,会把软引用回收
byte[] b = new byte[1024*1024*15];
// 此时软引用指向的对象已经不存在了
System.out.println(o.get());
// 但是软引用对象仍然健在
System.out.println(o);
}
}
2.3 使用场景
可以用作缓存,如果内存够用,则继续缓存保留加快程序运行速度,如果需要创建对象需要更多的空间,暂时牺牲运行速度,别爆出OOM错误,干掉软引用指向的缓存。
3 弱引用
3.1 什么是弱引用
简单说就是当发生GC时,不管堆空间剩下多少、够不够用,都回收掉弱引用指向的对象(而不是回收弱引用本身)。
3.2 代码实验
public class WeakReference {
public static void main(String[] args) {
WeakReference<Obejct> o = new WeakReference<>(new Obejct());
// 仍能获取到弱引用指向的对象
System.out.println(o.get());
System.gc();
// 弱引用指向的对象经过 GC之后被回收
System.out.println(o.get());
// 但是弱引用对象本身并没有被回收
System.out.println(o);
}
}
3.3 使用场景
3.3.1 ThreadLocal
源码分析
每个线程都有自己的 ThreadLocal
作为线程本地存储变量,有一个内部类THreadLocalMap
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
threadLocals
在每次new Thread(Runnable)
的时候,都会有一个自己的ThreadLocalMap
类型的threadLocals
对象。
class ThreadLocal{
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//新创建一个Entry对象, Entry这个类很有意思
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 把键值对放到table中
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocalMap
又使用一个关键的对象 table
来进行键值对的存储,所以关键点还是得看 Entry
这个类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
此时我们发现,Entry
是一个弱引用的子类,这个弱引用“弱弱的”指向了一个ThreadLocal
对象
OK!此时我们再用一段很简单的Java代码来看一下现在的关系:
3.3.2 ThreadLocal
小实验
public class WeakReferenceAndThreadLocal {
public static void main(String[] args) {
ThreadLocal<Obejct> tl = new ThreadLocal<>();
tl.set(new Object());
tl.remove();
}
}
我们逐句来解释每句话到底是什么意思:
tl.set(new Object());
- 往main线程的
ThreadLocalMap
中放入一个new Object
,set()
方法最终调用如下:map.set(this, value);
,在这个Map中,刚刚创建的tl
作为key(而且这个key指向所用的引用类型是弱引用),new Object
作为 V
- 往main线程的
此时各个对象之间的引用关系如下图所示
-
tl.remove();
-
最终调用
m.remove(this);
,-
private void remove(ThreadLocal<?> key) { ... for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //断掉K的弱引用 e.clear(); //断掉V的强引用,移除value占用的槽位 expungeStaleEntry(i); return; } } } public void clear() { this.referent = null; } private int expungeStaleEntry(int staleSlot) { ... tab[staleSlot].value = null; tab[staleSlot] = null; ... }
-
-
方法栈帧弹出后,tl
消失,强引用消失,之前map的key的弱引用和value的强引用也消失。
3.4 ThreadLocal
的内存泄漏问题
K的弱引用换成强引用
假设我们K的虚引用变成了强引用,而且恰巧因为程序员的失误没有调用remove方法,那么即使这个创建ThreadLocal
对象的方法从栈帧弹出,也仍然无法对K-V所指向的对象进行垃圾回收。
K仍然弱引用,但是没有调用remove方法
程序庞大运行时间久之后,结果就会导致这个threadLocals
这个map
结构越来越庞大,但是V指向的对象却已经获取不到,因为虚引用指向K对象已经被GC回收,从而造成了内存泄漏问题
4. 虚引用
// TODO