Java中引用类型,可能有一定开发经验人,才会了解到这一步,本文将从基础开始,随后结合jdk及框架中对引用使用、OOM分析 深入探讨Java中引用类型。
介绍
背过面试题的都知道,Java中有四种引用类型:
- 强引用:就算oom,也不回收
- 软引用
SoftReference
:要oom时候,才回收 - 弱引用
WeakReference
:gc就回收 - 虚引用``:每次get方法,都是null,用途是作为gc时候的通知。
Reference 继承结构
几种引用类型,都是继承 Reference
。
其中,软引用构造时,无法传递ReferenceQueue队列:
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
弱引用和虚引用,在构造时,需要传递 ReferenceQueue
供监听回收通知:
弱引用:
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
对于弱引用,get方法会直接获取reference值并返回。
虚引用:
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
虚引用的get方法,则永远返回null,即永远不可达
public T get() {
return null;
}
Reference
看看里面变量:
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */ // 实际的引用
volatile ReferenceQueue<? super T> queue; // 缓存queue对象
volatile Reference next; // 下一个节点
transient private Reference<T> discovered; /* used by VM */
static private class Lock { }
private static Lock lock = new Lock();
private static Reference<Object> pending = null;
}
next
:这个字段在不同情况下,指向对象不同,按照java doc描述如下:
/* When active: NULL
* pending: this
* Enqueued: next reference in queue (or this if last)
* Inactive: this
*/
discovered
: 由vm维护,在active和pending状态时,维护不同队列,分以下几种情况:
/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/
pending
:pending作为静态变量,应用唯一,可以理解为头结点,由vm将reference放入pending队列中,由discovered连接,而最终由Reference-handler
线程将对应待丢弃对象放入对应的ReferenceQueue当中。
Java中Reference引用整体扭转流程如下图所示:
- 从Active到Pending状态,是有vm经过可达性分析之后,丢到Pending队列中,使用discover连接。
- 从Pending到Enqueued 状态,则是由
ReferenceHandler
线程进行调度,ReferenceHandler
具有最高优先级。 - 当对象从queue中删除时,变为Inactive状态,才会真正被垃圾回收掉,如果是使用
SoftReference
,当可达性分析不存在时,则直接进入Inactive状态。
ReferenceQueue
在Java引用中 ,ReferenceQueue
角色其实是维护者一个head 头结点的角色,里面的enqueue方法是采用头插法构造队列的。
ReferenceHandler
看看从Pending到Enqueued 状态,java程序是如何处理的:
在Reference静态代码块中,生成并启动ReferenceHandler:
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY); // 最高优先级
handler.setDaemon(true); // 守护线程
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
循环尝试处理:
public void run() {
while (true) {
tryHandlePending(true);
}
}
tryHandlePending方法:
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 判断该reference是否为Cleaner
c = r instanceof Cleaner ? (Cleaner) r : null;
// pending指向r的下一个待pending的节点
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// 让出cpu,相信gc会成功挺过这次oom?
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// 优先clean
if (c != null) {
c.clean();
return true;
}
// 把r放入到r对应的ReferenceQueue中
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
Cleaner
Cleaner 实际上是一个虚引用PhantomReference
,Cleaner可以理解为JDK的亲儿子,ReferenceHandler 中对Cleaner的操作明显有点护犊子,在JDK代码中,主要是nio直接内存的使用有维护:
Cleaner有以下特性:
- 自己维护这一个双向链表,有一个静态变量first指代头节点,next和prev分别指向前驱和后继节点。
- 同样是采用头插法进行维护
- 构造方法属性为private,且接受一个Runnable类型变量,在最终执行clean方法时,直接执行run方法进行clean。这样虽然可以由开发者自定义实现清除函数,但是也带来了不确定性:当一个clean线程执行时间过长,会导致整个ReferenceHandler 后续等待节点饥饿。
所以大多数情况下,程序员可以自行 通过 继承 PhantomReference
实现这样的功能。
FinalReference
Reference
还有一个子类 FinalReference
,由于该引用是包可见性,所以 Java中对他的使用,也仅仅局限于 Finalizer
类,这个类和面试中一直在背过的finalize
就有关了。
背过的面试题:
-
finalize
方法,功能类似于c++中析构函数,但又不是析构函数。 -
finalize
执行线程优先级低,可能被执行,也可能不被执行,并且一个对象只会有一次执行的机会,即复活一次?
大多数特性都可以从 Finalizer
中代码体现出来。
如果某一个类有重写finalize
方法时,当jvm认为该对象没有没有其他引用时,就会调用finalize方法,可以给该类再次赋予引用,避免被gc,但是一个对象只有一次执行finalize机会。
Finalizer
final class Finalizer extends FinalReference<Object> {
}
包可见,且为final 类型,无法被子类重写。
Finalizer 构造方法为私有,且只能有vm调用:
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
当jvm初始化实例时,发现类重写了finaize方法,则会调用register,将该对象通过尾插法插入到Finalizer队列中:
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
Finalizer有以下字段属性:
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null; // 指向最后一个节点,
private static final Object lock = new Object(); // 自定义锁
private Finalizer
next = null,
prev = null;
Finalizer内部维护和一条双向链表,且unfinalized指向最后一个节点,节点之间通过next和prev连接。
FinalizerThread
类似于引用将pending状态节点入队,Finalizer也有一个线程 FinalizerThread
,他负责将所有没有引用的对象出队并执行finalize 方法,但是该线程优先级要比ReferenceHandler
低。
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2); // 比最大优先级低
finalizer.setDaemon(true);
finalizer.start();
}
FinalizerThread
的 run
方法(截取核心):
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove(); // 从引用队列中获取对象
f.runFinalizer(jla);
} catch (InterruptedException x) { // 忽略interrupt异常
// ignore and continue
}
}
runFinalizer方法:
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
从上面代码可以看出运行finalize方法有如下特性:
- 锁住当前Reference对象,如果next=null,即证明已经不在finalizer队列中,则不在执行,因为入队入口是唯一的,所以当一个对象执行过finalize方法后,就出队了,所以当又要被gc时,不会再执行finalize方法了。
- 获取Reference对象,执行finalize方法。
- catch 住 Throwable 异常,即忽略所有异常。最后调用父类clear方法,将reference置为null。
- 优先级低,可能不会被执行
总结
- Java虽然有四种引用类型,但是使用不当将会引发内存泄漏,因为到ReferenceQueue时,会临时变为强引用类型。
- 对于这些引用,都要经过至少2次GC,才能被回收。
觉得对你有帮助?不如关注博主公众号: 六点A君