最近在知乎上看到的一个写的不错的文章,记录其中关键的知识点,以防忘记。
WeakReference中所有的方法都是继承自Reference,所以要了解WeakReference以及更多其他的SoftReference、PhtomReference,学习好Reference是基础。
弱引用 被GC特殊对待原理
Reference有四个成员变量
这四个成员变量不能改变他们的顺序,因为它关系到JVM GC机制,至少Hotspot是这么做的。
Hotspot gc发生时,会对GC roots进行遍历,其中遍历一个GC root能达到的它的引用时,是使用了OopMapBlock,简单说,WeakReference Class的OopMapBlock是这样:
但hotspot在Universe的初始化阶段,会调用一个函数,这个函数会调整Class中定义的field在OopMapBlock的属性(具体,可参加GC tracing的引用在上图中,一开始OopMapBlock(2,4), 其中2为Offset从reference开始,4为size大小, 修改之后变为OopMapBlock(3, 1), 即从queue开始,也只遍历这一个field),现实中就会导致这些Field不会被GC时分析可达性的时候被扫描到。在Reference中只有queue一个会被扫描,做可达性分析,所以例如referent是不会被标记可达的。
弱引用 GC过程
弱引用在GC的过程中,会被特殊处理。在GC的过程中,以copy gc为例,所有存货的强引用都会被拷到新的survivor区域中,但是弱引用不会被拷贝。同时会使用WeakReference的discovered域将其串联起来。然后在JVM中,会启动一个线程,叫做Reference Handler(这个就定义在Reference类中,JVM加载Reference class的时候,就会启动这个线程)
若一个对象只有被弱引用的情况下,就会被回收,若不是则对象的新地址会更新到WeakRefernce中的reference中去。
pending是由JVM维护的,可通过Reference中的discovered field找到下一个满足回收条件的Reference。ReferenceHandler负责将JVM维护的pending reference queue 取出来然后将本身所在的Reference对象加入自身的Reference Queue中,这个设计很巧妙,就像上面所提到的,垃圾回收只会去扫描Reference的queue field,当确定好reference的referent字段没有被其他的类引用的时候,就会将自身加入Reference queue中,下次垃圾回收的时候一起回收掉这些引用类。
SoftReference VS WeakReference
SoftReference与WeakReference的处理大致相同,唯一不同的是,它仅仅在是否把Reference添加到链表离,多增加了一些判断,那就是当前引用的存货时间是不是大于_max_interval,如果大于,那它就和WeakReference一样处理,如果不大于的话,那就当成普通的强引用处理。
// Capture state (of-the-VM) information needed to evaluate the policy
void LRUMaxHeapPolicy::setup() {
size_t max_heap = MaxHeapSize;
max_heap -= Universe::get_heap_used_at_last_gc();
max_heap /= M;
_max_interval = max_heap * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
上述代码记录了_max_interval的计算方式,也就是说,max_heap越小,_max_interval就会越小,SoftReference就会有越大的可能性被回收。SoftRefLRUPolicyMSPerMB可通过调节JVM启动参数调整,这个参数越小,SoftReference就会越倾向于尽快回收。
PhantomReference & Cleaner
PhantomReference 虚引用,它继承于Reference,方法很简单:
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get() {
return null;
}
/**
* Creates a new phantom reference that refers to the given object and
* is registered with the given queue.
*
* <p> It is possible to create a phantom reference with a <tt>null</tt>
* queue, but such a reference is completely useless: Its <tt>get</tt>
* method will always return null and, since it does not have a queue, it
* will never be enqueued.
*
* @param referent the object the new phantom reference will refer to
* @param q the queue with which the reference is to be registered,
* or <tt>null</tt> if registration is not required
*/
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
Cleaner继承于Phantom Reference,在Reference Handler里面,我们也看到,Reference Handler会判断Reference的类型,如果是Cleaner,那么就直接调用它的clean方法。具体的清理逻辑会写在cleaner中的Runnable接口中,使用者实现它的run方法。
FinalReference 和 finalize 方法
finalize方法定义在Object中:
protected void finalize() throws Throwable {}
在对象中可以重写这个方法,在这个方法中可以释放各种资源。具体做法是,hotspot会把类的构造方法的最后一条指令,也就是Java虚拟机指令的return,重写为一个特殊的指令,具体代码省略,涉及到C++。大致流程就是,如果Class中定义了finalize方法,就会调用register_finalizer这个方法。最终会调用到Finalizer.java中定义的:
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
生成一个Finalizer对象,而我们自己的对象就是这个方法中所使用的finalizee。
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
这个queue,类比于Reference Handler线程,那个线程会把所有的Reference从pending链表上取出来,然后加入到一个queue中。对于FinalReference,就都会加入到上述代码中的queue。这个queue中的所有对象,都会被一个名为FinalizerThread所处理,在FinalizerThread中调用它的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();
}
这个方法使用了JavaLangAccess作为参数,其实就是一次虚函数调用,调用到finalizee中的finalize方法。hasBeenFinalized的作用就是保证finalize方法只会被调用一次。
Fianlizer与Cleaner最大的不同是:
public void finalize() {
Other.ref = this; // 这里Other.ref是任何其他的类的引用
}
这样一个本该回收的对象又在finalize之后复活了。但Cleaner不行,因为Cleaner继承于PhantomReference,它的get方法始终返回null。其实从被创建之后,它就从外面取不到自己的Referent。所以说cleaner比finalizer用起来安全,finalizer如果在finalize方法中又将自身的referent交给其他类的引用,那么将再次复活它,这样又因为finalizer对于每个reference只会执行一回收,那么可想之后就再也不会对其回收了,这样就导致了内存泄露。再有,finalizer的线程优先级也没有Cleaner高。