概诉
上一篇介绍了GC的各种垃圾收集器的算法,本节详细讨论几个细节问题。
GC Roots详解
GC ROOTs包含了那些对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI (即 native 方法)引用的对象。
- 分代回收算法中非当前GC年代的其他对象。
如何识别“垃圾对象”?
为什么需要引用?
如前所述:在最早的JVM实现里,使用“跟踪回收”算法从GC ROOTS出发,按照广度或者深度方式遍历所有与GC ROOTS可达的对象,针对那些GC不可达的垃圾对象进行回收。但随着Java的演进,针对最早这种比较简单的GC方式逐渐暴露出一些不能覆盖的情景,比如:某些场景下使用方希望在回收具体对象的同时还能辅助回收这个对象绑定的一些资源(比如socket、堆外内存等)、某些场景下希望使用的堆内缓存组件能尽量缓存更多的数据但又不会导致OOM。考虑到上述类似的使用场景,从JDK 1.2开始,JDK引入了软引用(SoftReference)、弱引用(WeakReference)、幻象引用(PhantomReference)、Final引用(FinalReference)四种新的引用来支持一些新的特性。
对象创建时,可以通过不同的引用来对对象进行封装,GC时,当真正的业务对象除了这个引用外没有其他GC ROOTS可达的时候,JVM会根据引用类型和GC类型(是否是Full GC)来对这个“真实对象”进行一些特殊处理,
比如:回收这个对象绑定的其他资源、比如根据GC类型来觉得是否本次要把对象进行回收等。

GC如何识别和处理引用?
“真实对象”创建时,可以通过Reference来对“真实对象”进行封装引用,然后通过一些方法保证这个引用GC ROOTS可达(比如封装完“真实对象”后将这个引用加入到一个静态链表中),JVM垃圾回收器硬编码识别
- SoftReference,
- WeakReference,
- PhantomReference,
- Final引用(FinalReference)
这些具体的类,GC过程中,识别查找到引用对象没有被其他强引用使用的Reference,然后添加到Reference类的pending链表,这个pending链表由GC来维护,通过Reference类的一个静态的pending变量(链表头)和一个实例变量discovered(链表下一节点)来实现;Reference有一个高优先级的ReferenceHandler线程,这个线程不停的从pending链表中取出待处理的Reference进行处理:有的放到Reference各自的ReferenceQueue队列里供使用者进行处理(如:PhantomReference和WeakReference)、有的直接调用固定的处理方法进行清理(如:Cleaner)。
Reference类:
1 | public abstract class Reference<T> { |
1 | //类加载完就起好最高优先级的ReferenceHandler |
ReferenceQueue可以由使用方通过Reference的构造方法指定传入,如果没有指定,从pending链表取出的Reference都enqueue到全局的一个ENQUEUED队列中。ReferenceQueue的实现:
1 | public class ReferenceQueue<T> { |
引用种类
强引用
最常见的引用方式。如:
1 | Object obj = new Object(); |
上面的obj就是一个指向Object对象的强引用
强引用如果有到GC ROOTS的路径,那么这个引用指向的对象在GC时不能被回收。
在实际使用中,除了强引用,可能还需要一些其他特殊类型的引用,比如有些缓存对象,是可以在内存不足时来回收的,这样通过丰富的引用类型,能让内存在实际使用时更灵活,整体业务稳定性更好。jdk 1.2后,对引用概念进行了扩充,增加了其他4种类型的引用,在java.lang.ref包下,分别为:
SoftReference、WeakReference、PhantomReference、FinalReference。
软引用(SoftReference)
1 | SoftReference<String> str = new SoftReference<String>("abc"); |
软引用是比强引用稍弱的一种引用,普通的GC并不会回收软引用,只有在即将OOM的时候(也就是最后一次Full GC)的时候才会回收软引用指向的对象。所以,软引用比较适合用来实现不是特别重要的缓存,比如guava cache就支持软引用类型的存储值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//如果只是普通增量GC,不回收软引用
if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) {
gch->do_collection(false /* full */,
false /* clear_all_soft_refs */,
size /* size */,
is_tlab /* is_tlab */,
number_of_generations() - 1 /* max_level */);
...
//否则,触发Full GC,但还不回收软引用
} else {
gch->do_collection(true /* full */,
false /* clear_all_soft_refs */,
size /* size */,
is_tlab /* is_tlab */,
number_of_generations() - 1 /* max_level */);
}
...
//如果还是内存不够时,会触发一次回收所有软引用的Full GC,再不行就OOM
gch->do_collection(true /* full */,
true /* clear_all_soft_refs */,
size /* size */,
is_tlab /* is_tlab */,
number_of_generations() - 1 /* max_level */);
}
弱引用(WeakReference)
弱引用比软引用的引用级别更低一些,GC时,如果一个对象从GC ROOTS出发,只有弱引用指向没有其他(强引用或软引用)指向时,这个对象就会在本次GC被回收掉。
弱引用最常见的使用情景是WeakHashMap,WeakHashMap里面的Entry是一个弱引用,这个弱引用指向Map的Key,如果这个Key没有被其他”强引用“或者”软引用“引用时,GC会干掉这个Key对象,同时将这个Entry对象放入WeakHashMap的ReferenceQueue中等待被处理,当WeakHashMap的get、put等方法被调用时,会通过expungeStaleEntries方法把这个ReferenceQueue的Entry对象的value置空并调整Entry链表摘取当前Entry,这样下次GC时就能回收掉value的对象了。
一般在需要控制内存使用但又想尽量用到更多内存的场景下使用。比如tomcat的ConcurrentCache就用到了WeakHashMap来作为分级缓存,可以在内存充足的情况下,缓存尽量多的数据,同时又不会导致OOM。
1 | private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { |
幻象引用(PhantomReference)
幻象引用是比弱引用的级别更低的一种,和软引用以及弱引用不同的是幻影引用指向的对象没有其他强引用、软引用指向时不会自动被GC清理。
PhantomReference的回收处理过程如下:
GC时,高优先级的ReferenceHandler线程将这个PhantomReference放到它自身的静态ReferenceQueue中,然后PhantomReference的实现子类一般会有一个线程在不断轮询这个ReferenceQueue,从queue中取出PhantomReference并调用它自己实现的清理方法来释放它所指向对象的占用的一些特定资源并把PhantomReference自身从ReferenceQueue中干掉,这样下次GC时这个幻象引用本身和它指向的对象也能够被GC掉。
实际场景中,一般幻象引用用于对象需要被清理时,除了指向的对象本身外,还需要额外释放这个对象占用的其他资源的场景。比如DB连接池使用PhantomReference来释放底层的socket资源,比如DirectByteBuffer使用PhantomReference来释放底层占用的堆外内存。
- 举例:
DirectByteBuffer使用PhantomReference管理堆外内存释放的过程如下:
- 每个DirectByteBuffer在生成时会绑定一个Cleaner对象,这个Cleaner对象是一个PhantomReference
- 当JVM GC时发现那些除了Cleaner幻象引用外已没有其他引用的DirectByteBuffer时,就会把这些Cleaner对象放到Reference这个类的pending列表里.
- Reference类维护了一条ReferenceHandler的高优先级线程,这条线程会不断去轮询待处理的pending列表,如果是Cleaner对象就调用这个对象的clean方法进行清理(这里需要注意的是:Cleaner是一种特殊的PhantomReference,它实际的清理工作是由ReferenceHandler线程直接执行的,不需要自己再维护一个清理的线程)
- clean方法里其实是调用初始化Cleaner时绑定的Deallocator间接使用unsafe.freeMemory来进行堆外内存的释放和Bits里全局堆外内存使用量的更新。
Final引用(PhantomReference)
FinalReference & Finalizer
在java.lang.ref包下,除了上面四种引用,还有一个非公开的FinalReference引用以及它的一个子类Finalizer。实际上,FinalReference 代表的正是 Java 中的强引用,如这样的代码 :
Bean bean = new Bean();
在虚拟机的实现过程中,实际采用了 FinalReference 类对其进行引用。而 Finalizer,除了作为一个实现类外,更是在虚拟机中实现一个 FinalizerThread,以使虚拟机能够在所有的强引用被解除后实现内存清理。
Finalizer的工作过程:
GC过程中,当一个强引用对象bean没有了引用被标记为可回收时,如果这个bean对象的类定义了finalize方法,那么这个对象被绑定到一个Finalizer引用上,这个Finalizer引用会被前面讲过的ReferenceHandler线程将引用自身加入到Finalizer静态的ReferenceQueue中,同时Finalizer对象自带的静态的优先级为8(比普通线程优先级高但比ReferenceHandler优先级低)的FinalizerThread线程会轮询这个ReferenceQueue中的Finalizer引用,然后调用它的runFinalizer方法,最终调到了绑定的那个bean对象的finalize方法,当finalize方法的逻辑都执行完后,这个bean对象才会在下次GC时被回收。这里有几个需要注意的点:
1. 在CPU资源比较紧张的情况下,由于FinalizerThread线程优先级较低,可能由于得不到时间片而导致finalize的方法执行延迟或者缓慢。最终可能导致bean对象真正占用的资源释放不确定性提高,另外也可能会导致由于bean对象无法回收导致Full GC甚至OOM。
2. 底层的bean对象至少需要2次GC才会被回收。第一次GC只是标记后将处理任务提交给FinalizerThread线程去执行,当FinalizerThread执行完finalize方法后才会在下次GC时回收bean对象,FinalizerThread执行期间可能经历多次GC。
3. 和PhantomReference不同,由于Finalizer引用最终的释放依赖对象的finalize方法的实现,在finalize里实际上可以访问到引用的对象本身,所以如果在finalize方法里让其他对象又引用了当前对象,这样会导致这个本应该被回收的对象复活。
高优先级的ReferenceHandler线程将Finalizer引用加入到Finalizer类的静态ReferenceQueue中
1 | static boolean tryHandlePending(boolean waitForNotify) { |
- FinalizerThread线程处理:
- 1. 将当前Finalizer引用从ReferenceQueue里删除
- 2. 执行Finalizer引用的runFinalizer来触发bean对象的清除操作
1 | private static class FinalizerThread extends Thread { |
jla.invokeFinalize实际调用了bean对象的finalize方法
1 | private void runFinalizer(JavaLangAccess jla) { |
- finalize方法里将其他引用指向bean对象本身,导致bean对象复活
1
2
3public void finalize() {
Other.ref = this;
}
而在PhantomReference实现中,由于get方法默认返回null,因此PhantomReference创建后就不能再访问它的referent,因此不存在死对象复活的问题。所以,尽量使用PhantomReference来替代原来Finalizer的功能。
1 | public class PhantomReference<T> extends Reference<T> { |
GC过程是如何具体处理引用?
GC时,各垃圾收集器都会在过程中,先找到当前回收代中那些”指向的referent除了当前引用外没有其他强引用使用了”的引用,然后对所有类型的Reference(soft、weak、phantom、final)进行进一步筛选和排除处理,最后,在GC的最后阶段,将待处理的Reference入队到Reference的pendingList等待ReferHandler线程来善后。
发现引用
“引用发现”这个过程的主要工作:_找出当前回收代中的可能需要被回收的这些引用。
如ParNew垃圾回收器中的Ref»发现-标记»过程(parNewGeneration.cpp):
先处理roots触发,引用到的对象都拷贝到to space,然后通过par_scan_state.evacuate_followers_closure().do_void()在to space里遍历全部年轻代存活对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15gch->gen_process_roots(_gen->level(),
true, // Process younger gens, if any,
// as strong roots.
false, // no scope; this is parallel code
GenCollectedHeap::SO_ScavengeCodeCache,
GenCollectedHeap::StrongAndWeakRoots,
&par_scan_state.to_space_root_closure(),
&par_scan_state.older_gen_closure(),
&cld_scan_closure);
par_scan_state.end_strong_roots();
// "evacuate followers".
par_scan_state.evacuate_followers_closure().do_void();EvacuateFollowersClosureGeneral::do_void方法主要执行父类DefNewGeneration的oop_since_save_marks_iterate方法。
1 | void DefNewGeneration:: \ |
- .space的oop_since_save_marks_iterate会调用每一个对象的oop_iterate
1 | void ContiguousSpace:: \ |
- .oop是堆上对象的基类,它的oop_iterate实际上会调到对应类的oop_oop_iterate##nv_suffix方法,对于引用类型的对象,会走到InstanceRefKlass的op_oop_iterate##nv_suffix的方法。
1 | inline int oopDesc::oop_iterate(OopClosureType* blk) { \ |
- InstanceRefKlass的oop_oop_iterate##nv_suffix会调用InstanceRefKlass_SPECIALIZED_OOP_ITERATE这个语句块。
1 | int InstanceRefKlass:: \ |
- 在InstanceRefKlass_SPECIALIZED_OOP_ITERATE这个语句块中,这里会执行最终的»引用发现»discover_reference方法。
1 |
|
discover_reference方法中,只收集那些还“存活的”、“reference是在当前回收代的”、“引用的真实对象还不确定是否被其他强引用的”的引用,会真正操作Reference的discovered字段来维护»发现»的引用链表。
PS:这里“发现”引用的阶段有两种情况需要考虑:
1. 如果是引用指向的对象先被GC扫描到被强引用了,那么引用以及它所指向的对象都不会被“发现”(而是随后连同引用和指向对象都被copy到To区)。
2. 如果是引用本身先被GC扫描到,那么这里的“发现”阶段还是会把引用先加入到discovered链表中并将引用搬到To区(但不动引用指向的对象),等GC后,在后面“处理”引用的阶段会遍历这个discovered链表,找出那些除了自己外还有其他“强引用”指向referent对象的引用(这个referent对象由于有强引用已经被copy到To区了),然后从discovered链表删除自己并更新引用指向的对象地址,这样后续就不用再处理了。
1 | bool ReferenceProcessor::discover_reference(oop obj, ReferenceType rt) { |
处理引用
针对之前»发现»的引用,GC过程还会通过ReferenceProcessor的process_discovered_references来对所有类型的Reference(soft、weak、phantom、final)进行处理,处理步骤分成3个阶段,主要工作:根据GC策略来过滤软引用、过滤GC后那些referent还存活的引用 、根据不同引用类型决定是否马上解除对referent的引用(如弱引用、软引用和Cleaner在这里直接清理了对referent的引用,而幻象引用和Final引用这里先不清除,因为后面还需要用到)。
还是是ParNew回收器为例,GC时,在完成“引用发现”后,会通过process_discovered_references方法对引用进行处理,最后GC完成内存回收后再将Reference加入到pending列表中以便后续处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31void ParNewGeneration::collect(bool full,
bool clear_all_soft_refs,
size_t size,
bool is_tlab) {
//...
//处理引用
if (rp->processing_is_mt()) {
ParNewRefProcTaskExecutor task_executor(*this, thread_state_set);
stats = rp->process_discovered_references(&is_alive, &keep_alive,
&evacuate_followers, &task_executor,
_gc_timer, gc_tracer.gc_id());
} else {
thread_state_set.flush();
gch->set_par_threads(0); // 0 ==> non-parallel.
gch->save_marks();
stats = rp->process_discovered_references(&is_alive, &keep_alive,
&evacuate_followers, NULL,
_gc_timer, gc_tracer.gc_id());
}
//...
//通过将当前引用列表附到原来的pending链表以便ReferenceHandler线程的善后处理
rp->set_enqueuing_is_done(true);
if (rp->processing_is_mt()) {
ParNewRefProcTaskExecutor task_executor(*this, thread_state_set);
rp->enqueue_discovered_references(&task_executor);
} else {
rp->enqueue_discovered_references(NULL);
}
//...
}
// ...
GC完成时,处理”被发现“的引用的方法process_discovered_references主要实现在referenceProcessor.cpp的process_discovered_reflist中,包括三个阶段。
- 1. 第一个阶段只处理软引用:因为软引用普通GC时是不能回收处理的,所以需要从discovered链表中移除所有不存活但是还不能被回收的软引用;
- 2. 第二阶段处理所有引用:从discoverd链表移除那些GC时»发现»阶段还不确定referent有没有被其他强引用但现在GC遍历完了确定»有被强引用»的引用。
- 3. 第三阶段处理剩下引用的referent:根据clear_referent的值决定是否将对referent的引用解除,方便下一次GC时回收referent。 (比如Final和Phantom是先不解除引用的,因为后续还要用;Weak是可以解除的)。
1 | ReferenceProcessor::process_discovered_reflist( |
处理完引用后,GC回收完内存后,在结束阶段会通过ReferenceProcessor的enqueue_discovered_references方法来将处理过的引用通过pending链表来进行入队列,这样ReferenceHandler才能把队列里的Reference对象从pending链表取出后写入到ReferenceQueue或者进行clean(Cleaner)操作。enqueue_discovered_references会根据是否使用压缩指针选择不同的enqueue_discovered_ref_helper()模板函数。
1 | bool ReferenceProcessor::enqueue_discovered_references(AbstractRefProcTaskExecutor* task_executor) { |
pending_list_addr是Reference类的pending链表的首元素地址,enqueue_discovered_reflists过程会把符合的引用加入到这个链表,ReferenceHandler则从pending链表取出引用后放入ReferenceQueue或直接处理(Cleaner)。
1 | bool enqueue_discovered_ref_helper(ReferenceProcessor* ref, |
- 多线程和单线程处理方式,由-XX:+ParallelRefProcEnabled控制,默认单线程,实际处理代码在enqueue_discovered_reflist中,主要逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32void ReferenceProcessor::enqueue_discovered_reflist(DiscoveredList& refs_list,
HeapWord* pending_list_addr) {
if (pending_list_uses_discovered_field()) { // New behavior
// Walk down the list, self-looping the next field
// so that the References are not considered active.
while (obj != next_d) {
obj = next_d;
assert(obj->is_instanceRef(), "should be reference object");
next_d = java_lang_ref_Reference::discovered(obj);
if (TraceReferenceGC && PrintGCDetails) {
gclog_or_tty->print_cr(" obj " INTPTR_FORMAT "/next_d " INTPTR_FORMAT,
(void *)obj, (void *)next_d);
}
assert(java_lang_ref_Reference::next(obj) == NULL,
"Reference not active; should not be discovered");
// Self-loop next, so as to make Ref not active.
java_lang_ref_Reference::set_next_raw(obj, obj);
if (next_d != obj) {
oopDesc::bs()->write_ref_field(java_lang_ref_Reference::discovered_addr(obj), next_d);
} else {
// This is the last object.
// Swap refs_list into pending_list_addr and
// set obj's discovered to what we read from pending_list_addr.
oop old = oopDesc::atomic_exchange_oop(refs_list.head(), pending_list_addr);
// Need post-barrier on pending_list_addr. See enqueue_discovered_ref_helper() above.
java_lang_ref_Reference::set_discovered_raw(obj, old); // old may be NULL
oopDesc::bs()->write_ref_field(java_lang_ref_Reference::discovered_addr(obj), old);
}
//...
}
// ...
}
赞赏一下
