Java引用体系中我们最熟悉的就是强引用类型,如 A a= new A();这是我们经常说的强引用StrongReference,jvm gc时会检测对象是否存在强引用,如果存在由根对象对其有传递的强引用,则不会对其进行回收,即使内存不足抛出OutOfMemoryError。
除了强引用外,Java还引入了SoftReference,WeakReference,PhantomReference,FinalReference ,这些类放在java.lang.ref包下,类的继承体系如下图。Java额外引入这个四种类型引用主要目的是在jvm 在gc时,按照引用类型的不同,在回收时采用不同的逻辑。可以把这些引用看作是对对象的一层包裹,jvm根据外层不同的包裹,对其包裹的对象采用不同的回收策略,或特殊逻辑处理。 这几种类型的引用主要在jvm内存缓存、资源释放、对象可达性事件处理等场景会用到。
名称说明下:Reference指代引用对象本身,Referent指代被引用对象,下文介绍会以Reference,Referent形式出现。
下面我们先介绍一下Java对象可达性判断逻辑和ReferenceQueue,然后依次对这四种引用使用和作用进行说明。
对象可达性判断
jvm gc时,判断一个对象是否存在引用时,都是从根结合引用(Root Set of References)开始去标识,往往到达一个对象的引用路径会存在多条,如下图。
那么 垃圾回收时会依据两个原则来判断对象的可达性:
单一路径中,以最弱的引用为准
多路径中,以最强的引用为准
例如Obj4的引用,存在3个路径:1->6、2->5、3->4, 那么从根对象到Obj4最强的引用是2->5,因为它们都是强引用。如果仅仅存在一个路径对Obj4有引用时,比如现在只剩1->6,那么根对象到Obj4的引用就是以最弱的为准,就是SoftReference引用,Obj4就是softly-reachable对象。
ReferenceQueue VS Reference
Reference作为SoftReference,WeakReference,PhantomReference,FinalReference这几个引用类型的父类。主要有两个字段referent、queue,一个是指所引用的对象,一个是与之对应的ReferenceQueue。Reference类有个构造函数 Reference(T referent, ReferenceQueue<? super T> queue),可以通过该构造函数传入与Reference相伴的ReferenceQueue。
ReferenceQueue本身提供队列的功能,有入队(enqueue)和出队(poll,remove,其中remove阻塞等待提取队列元素)。ReferenceQueue对象本身保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。同时ReferenceQueue提供了两个静态字段NULL,ENQUEUED
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
这两个字段的主要功能:NULL是当我们构造Reference实例时queue传入null时,会默认使用NULL,这样在enqueue时判断queue是否为NULL,如果为NULL直接返回,入队失败。ENQUEUED的作用是防止重复入队,reference后会把其queue字段赋值为ENQUEUED,当再次入队时会直接返回失败。
boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
synchronized (lock) {
// Check that since getting the lock this reference hasn’t already been
// enqueued (and even then removed)
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
r.queue = ENQUEUED;
r.next = (head == null) ? r : head;
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
Reference与ReferenceQueue之间是如何工作的呢?Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler
thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。我们可以通过下面代码块来进行把SoftReference,WeakReference,PhantomReference与ReferenceQueue联合使用来验证这个机制。为了确保SoftReference在每次gc后,其引用的referent都被回收,我们需要加入-XX:SoftRefLRUPolicyMSPerMB=0参数,这个原理下文中会在讲。
/**
* 为了确保System.gc()后,SoftReference引用的referent被回收需要加入下面的参数
* -XX:SoftRefLRUPolicyMSPerMB=0
*/
public class ReferenceTest {
private static List<Reference> roots = new ArrayList<>();
public static void main(String[] args) throws Exception {
ReferenceQueue rq = new ReferenceQueue();
new Thread(new Runnable() {
@Override
public void run() {
int i=0;
while (true) {
try {
Reference r = rq.remove();
System.out.println(“reference:”+r);
//为null说明referent被回收
System.out.println( “get:”+r.get());
i++;
System.out.println( “queue remove num:”+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
for(int i=0;i<100000;i++) {
byte[] a = new byte[1024*1024];
// 分别验证SoftReference,WeakReference,PhantomReference
Reference r = new SoftReference(a, rq);
//Reference r = new WeakReference(a, rq);
//Reference r = new PhantomReference(a, rq);
roots.add(r);
System.gc();
System.out.println(“produce”+i);
TimeUnit.MILLISECONDS.sleep(100);
}
}
}
通过jstack命令可以看到对应的Reference Handler thread
“Reference Handler” #2 daemon prio=10 os_prio=31 tid=0x00007f8fb2836800 nid=0x2e03 in Object.wait() [0x000070000082b000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
– waiting on <0x0000000740008878> (a java.lang.ref.ReferenceLock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
因此可以看出,当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时(或者要被回收 如下文要讲的Finalizer),提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。当然,如果我们不需要这种通知机制,我们就不用传入额外的queue,默认使用NULL queue就会入队失败。
SoftReference
根据上面我们讲的对象可达性原理,我们把一个对象存在根对象对其有直接或间接的SoftReference,并没有其他强引用路径,我们把该对象成为softly-reachable对象。JVM保证在抛出OutOfMemoryError前会回收这些softly-reachable对象。JVM会根据当前内存的情况来决定是否回收softly-reachable对象,但只要referent有强引用存在,该referent就一定不会被清理,因此SoftReference适合用来实现memory-sensitive caches。软引用的回收策略在不同的JVM实现会略有不同,javadoc中说明:
Virtual machine implementations are, however, encouraged to bias against clearing recently-created or recently-used soft references.
也就是说JVM不仅仅只会考虑当前内存情况,还会考虑软引用所指向的referent最近使用情况和创建时间来综合决定是否回收该referent。
Hotspot在gc时会根据两个标准来回收:
根据SoftReference引用实例的timestamp(每次调用softReference.get()会自动更新该字段,把最近一次垃圾回收时间赋值给timestamp,见源码)
当前JVM heap的内存剩余(free_heap)情况
计算的规则是:
free_heap 表示当前堆剩余的内存,单位是MB
interval 表示最近一次GC’s clock 和 当前我们要判断的softReference的timestamp 差值
ms_per_mb is a constant number of milliseconds to keep around a SoftReference for each free megabyte in the heap(可以通过-XX:SoftRefLRUPolicyMSPerMB来设定)
那么判断依据就是: interval <= freeheap * ms_per_mb,如果为true,则保留,false则进行对象清除。 SoftReferences will always be kept for at least one GC after their last access。_ 因为 只要调用一次,那么clock和timestamp的值就会一样,clock-timestamp则为0,一定小于等于free_heap * ms_per_mb。 OpenJDK的大概referencePolicy.cpp代码是:
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”);
}
bool LRUMaxHeapPolicy::should_clear_reference(oop p,
jlong timestamp_clock) {
jlong interval = timestamp_clock – java_lang_ref_SoftReference::timestamp(p);
assert(interval >= 0, “Sanity check”);
// The interval will be zero if the ref was accessed since the last scavenge/gc.
if(interval <= _max_interval) {
return false;
}
return true;
}
可见,SoftReference在一定程度上会影响JVM GC的,例如softly-reachable对应的referent多次垃圾回收仍然不满足释放条件,那么它会停留在heap old区,占据很大部分空间,在JVM没有抛出OutOfMemoryError前,它有可能会导致频繁的Full GC。 github有个基于android源码LruCache改造成的 LruSoftCache,自行验证。
WeakReference
当一个对象被WeakReference引用时,处于weakly-reachable状态时,只要发生GC时,就会被清除,同时会把WeakReference注册到引用队列中(如果存在的话)。 WeakReference不阻碍或影响它们对应的referent被终结(finalized)和回收(reclaimed),因此,WeakReference经常被用作实现规范映射(canonicalizing mappings)。相比SoftReference来说,WeakReference对JVM GC几乎是没有影响的。
下面我们举个WeakReference应用场景,JDK自带的WeakHashMap,我们用下面的代码来测试查看WeakHashMap在gc后的entry的情况,加入-verbose:gc运行。
/**
* 加入下面参数,观察gc情况
* -verbose:gc
*/
public class WeakHashMapTest {
private static Map<String,byte[]> caches=new WeakHashMap<>();
public static void main(String[]args) throws InterruptedException {
for (int i=0;i<100000;i++){
caches.put(i+””,new byte[1024*1024*10]);
System.out.println(“put num: ” + i + ” but caches size:” + caches.size());
}
}
}
运行代码我们可以看到,虽然我们不断的往caches中put元素,但是caches size会伴随每次gc又从0开始了。
put num: 0 but caches size:1
put num: 1 but caches size:2
[GC (Allocation Failure) 23142K->20936K(125952K), 0.0199681 secs]
put num: 2 but caches size:1
put num: 3 but caches size:2
put num: 4 but caches size:3
[GC (Allocation Failure) 52293K->51672K(159232K), 0.0157178 secs]
put num: 5 but caches size:1
put num: 6 but caches size:2
put num: 7 but caches size:3
put num: 8 but caches size:4
put num: 9 but caches size:5
put num: 10 but caches size:6
[GC (Allocation Failure) 115728K->113064K(191488K), 0.0295324 secs]
[Full GC (Ergonomics) 113064K->61788K(237568K), 0.0172315 secs]
put num: 11 but caches size:1
put num: 12 but caches size:2
put num: 13 but caches size:3
put num: 14 but caches size:4
put num: 15 but caches size:5
put num: 16 but caches size:6
[GC (Allocation Failure) 124511K->123356K(291840K), 0.0174441 secs]
[Full GC (Ergonomics) 123356K->61788K(315392K), 0.0133423 secs]
put num: 17 but caches size:1
put num: 18 but caches size:2
put num: 19 but caches size:3
WeakHashMap实现原理很简单,它除了实现标准的Map接口,里面的机制也和HashMap的实现类似。从它entry子类中可以看出,它的key是用WeakReference包裹住的。当这个key对象本身不再被使用时,伴随着GC的发生,会自动把该key对应的entry都在Map中清除掉。它为啥能够自动清除呢?这就是利用上面我们讲的ReferenceQueue VS Reference的原理。WeakHashMap里声明了一个queue,Entry继承WeakReference,构造函数中用key和queue关联构造一个weakReference,当key不再被使用gc后会自动把把key注册到queue中:
/**
* Reference queue for cleared WeakEntries
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;
/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
//代码省落
}
}
WeakHashMap关键的清理entry代码:
/**
* Expunges stale entries from the table.
*/
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings(“unchecked”)
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size–;
break;
}
prev = p;
p = next;
}
}
}
}
这段代码会在resize,getTable,size里执行,清除失效的entry。
PhantomReference
PhantomReference 不同于WeakReference、SoftReference,它存在的意义不是为了获取referent,因为你也永远获取不到,因为它的get如下
public T get() {
return null;
}
PhantomReference主要作为其指向的referent被回收时的一种通知机制,它就是利用上文讲到的ReferenceQueue实现的。当referent被gc回收时,JVM自动把PhantomReference对象(reference)本身加入到ReferenceQueue中,像发出信号通知一样,表明该reference指向的referent被回收。然后可以通过去queue中取到reference,此时说明其指向的referent已经被回收,可以通过这个通知机制来做额外的清场工作。 因此有些情况可以用PhantomReference
代替finalize(),做资源释放更明智。
下面举个例子,用PhantomReference来自动关闭文件流。
public class ResourcePhantomReference<T> extends PhantomReference<T> {
private List<Closeable> closeables;
public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) {
super(referent, q);
closeables = resource;
}
public void cleanUp() {
if (closeables == null || closeables.size() == 0)
return;
for (Closeable closeable : closeables) {
try {
closeable.close();
System.out.println(“clean up:”+closeable);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class ResourceCloseDeamon extends Thread {
private static ReferenceQueue QUEUE = new ReferenceQueue();
//保持对reference的引用,防止reference本身被回收
private static List<Reference> references=new ArrayList<>();
@Override
public void run() {
this.setName(“ResourceCloseDeamon”);
while (true) {
try {
ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove();
reference.cleanUp();
references.remove(reference);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void register(Object referent, List<Closeable> closeables) {
references.add(new ResourcePhantomReference(referent,QUEUE,closeables));
}
}
public class FileOperation {
private FileOutputStream outputStream;
private FileInputStream inputStream;
public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) {
this.outputStream = outputStream;
this.inputStream = inputStream;
}
public void operate() {
try {
inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel());
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试代码:
public class PhantomTest {
public static void main(String[] args) throws Exception {
//打开回收
ResourceCloseDeamon deamon = new ResourceCloseDeamon();
deamon.setDaemon(true);
deamon.start();
// touch a.txt b.txt
// echo “hello” > a.txt
//保留对象,防止gc把stream回收掉,其不到演示效果
List<Closeable> all=new ArrayList<>();
FileInputStream inputStream;
FileOutputStream outputStream;
for (int i = 0; i < 100000; i++) {
inputStream = new FileInputStream(“/Users/robin/a.txt”);
outputStream = new FileOutputStream(“/Users/robin/b.txt”);
FileOperation operation = new FileOperation(inputStream, outputStream);
operation.operate();
TimeUnit.MILLISECONDS.sleep(100);
List<Closeable>closeables=new ArrayList<>();
closeables.add(inputStream);
closeables.add(outputStream);
all.addAll(closeables);
ResourceCloseDeamon.register(operation,closeables);
//用下面命令查看文件句柄,如果把上面register注释掉,就会发现句柄数量不断上升
//jps | grep PhantomTest | awk ‘{print $1}’ |head -1 | xargs lsof -p | grep /User/robin
System.gc();
}
}
}
运行上面的代码,通过jps | grep PhantomTest | awk ‘{print $1}’ |head -1 | xargs lsof -p | grep /User/robin | wc -l 可以看到句柄没有上升,而去掉ResourceCloseDeamon.register(operation,closeables);时,句柄就不会被释放。
PhantomReference使用时一定要传一个referenceQueue,当然也可以传null,但是这样就毫无意义了。因为PhantomReference的get结果为null,如果在把queue设为null,那么在其指向的referent被回收时,reference本身将永远不会可能被加入队列中,这里我们可以看ReferenceQueue的源码。
FinalReference
FinalReference 引用类型主要是为虚拟机提供的,提供 对象被gc前需要执行finalize方法的对象 的机制。
FinalReference 很简单就是extend Reference类,没有做其他逻辑,只是把访问权限改为package,因此我们是无法直接使用的。Finalizer类是我们要讲的重点,它继承了FinalReference,并且是final 类型的。Finalize实现很简单,也是利用上面我们讲的ReferenceQueue VS Reference机制。
FinalizerThread
Finalizer静态代码块里启动了一个deamon线程,我们通过jstack命令查看线程时,总会看到一个Finalizer线程,就是这个原因:
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方法是不断的从queue中去取Finalizer类型的reference,然后执行runFinalizer释放方。
public void run() {
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
runFinalizer方法体,执行事发逻辑,可以看出如果finalize方法中抛出异常会被直接吃掉:
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();
}
介绍完上面的处理机制,那么剩下的就是入queue的事情,就是哪些类对象需要入队,何时入队,下面我们一一介绍。
哪些类对象是Finalizer reference类型的referent呢
只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用的。下文中我们简称Finalizer 型的referent为finalizee。
何时调用Finalizer.register生成一个Finalizer类型的reference
Finalizer的构造函数是private的,也就是不能通过new 来生成一个Fianlizer reference。只能通过静态的register方法来生成。同时Finalizer有个静态字段unfinalized,维护了一个未执行finalize方法的reference列表,在构造函数中通过add()方法把Finalizer引用本身加入到unfinalized列表中,同时关联finalizee和queue,实现通知机制。维护静态字段unfinalized的目的是为了一直保持对未未执行finalize方法的reference的强引用,防止被gc回收掉。
private static Finalizer unfinalized = null;
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
那么register是被VM何时调用的呢?JVM通过VM参数 RegisterFinalizersAtInit 的值来确定何时调用register,RegisterFinalizersAtInit默认为true,则会在构造函数返回之前调用call_register_finalizer方法。
void Parse::return_current(Node* value) {
if (RegisterFinalizersAtInit &&
method()->intrinsic_id() == vmIntrinsics::_Object_init) {
call_register_finalizer();
}
…………..
}
如果通过-XX:-RegisterFinalizersAtInit 设为false,则会在对象空间分配好之后就调用call_register_finalizer
instanceOop InstanceKlass::allocate_instance(TRAPS) {
bool has_finalizer_flag = has_finalizer(); // Query before possible GC
int size = size_helper(); // Query before forming handle.
KlassHandle h_k(THREAD, this);
instanceOop i;
i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
if (has_finalizer_flag && !RegisterFinalizersAtInit) {
i = register_finalizer(i, CHECK_NULL);
}
return i;
}
何时入queue
当一个finalizee 只剩Finalizer引用,没有其他引用时,需要被回收了,GC就会把该finalizee对应的reference放到Finalizer的refereneQueue中,等待FinalizerThread来执行finalizee的finalize方法,然后finalizee对象才能被GC回收。
Finalizer问题
这里是列表文本 这里是列表文本finalizee对象在finalize重新被赋给一个强引用复活,那么下次GC前会不会被再次执行finalize方法?
答案是不会的,runFinalizer中会把该finalizee对应的Finalizer引用从unfinalized队列中移除,第二次执行的时会通过hasBeenFinalized方法判断,保证不会被重复执行。
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();
}
finalizee至少两次GC回收才可能被回收?
第一次GC把finalizee对应的Finalizer reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer reference对finalizee构成的是一个强引用。
Finalizer 机制导致JVM Full GC 频繁,stop-the-world延长?
因为如果finalizee上的finalize方法体执行过程耗时比较长,会导致对象一直堆积,多次GC仍不能释放,冲进old区,造成Old区GC过程延长,暂停时间增加,可能频繁触发Full GC。
总结
通过对SoftReference,WeakReference,PhantomReference,FinalReference 的介绍,可以看出JDK提供这些类型的reference 主要是用来和GC交互的,根据reference的不同,让JVM采用不同策略来进行对对象的回收(reclaim)。softly-reachable的referent在保证在OutOfMemoryError之前回收对象,weakly-reachable的referent在发生GC时就会被回收,finalizer型的reference 主要提供GC前对referent进行finalize执行机制。同时这些reference和referenceQueue在一起提供通知机制,PhantomReference的作用就是仅仅就是提供对象回收通知机制,Finalizer借助这种机制实现referent的finalize执行,SoftReference、WeakReference也可以配合referenceQueue使用,实现对象回收通知机制。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。