【061期】面试中经常被问到 Java 引用类型原理,深入源码剖析
>>号外:关注“Java精选”公众号,回复“面试资料”,免费领取资料!“”小程序,3000+ 道面试题在线刷,最新、最全 Java 面试题!
Java中一共有4种引用类型(其实还有一些其他的引用类型,比如FinalReference):强引用、软引用、弱引用、虚引用。
一个强引用就是我们经常使用的Object a = new Object(); 这样的形式,在Java中并没有的参考类。
本篇文章主要是分析软引用、弱引用、虚引用的实现,这三个引用类型都继承于引用类,主要逻辑化引用中。
问题
在分析前,先抛几个问题?
1.网上很多文章软引用的就是:在内存不足的时候才会被回收,那内存介绍不足是怎么定义的?什么才叫内存不足?
2.网上很多文章对于虚引用的介绍是:形同虚设,虚引用并不会决定对象的生命周期。主要收集对象被垃圾回收器回收的活动。真的是这样吗?
3.虚引用Jdk有哪些场景在下呢?
参考
我们先看下Reference.java中的几个字段
public abstract class Reference<T> {
//引用的对象
private T referent;
//回收队列,由使用者在Reference的构造函数中指定
volatile ReferenceQueue<? super T> queue;
//当该引用被加入到queue中的时候,该字段被设置为queue中的下一个元素,以形成链表结构
volatile Reference next;
//在GC时,JVM底层会维护一个叫DiscoveredList的链表,存放的是Reference对象,discovered字段指向的就是链表中的下一个元素,由JVM设置
transient private Reference<T> discovered;
//进行线程同步的锁对象
static private class Lock { }
private static Lock lock = new Lock();
//等待加入queue的Reference对象,在GC时由JVM设置,会有一个java层的线程(ReferenceHandler)源源不断的从pending中提取元素加入到queue
private static Reference<Object> pending = null;
}
一个参考对象的生命周期如下:
主要分为原生层和Java层两个部分。,内涵3000+道高级面试题,可在线随时刷看。
Native层在GC时将需要被回收的Reference对象加入到DiscoveredList中(代码在referenceProcessor.cpp中process_discovered_references方法),然后将DiscoveredList的元素移动到PendingList中(代码在referenceProcessor.cpp中enqueue_discovered_ref_helper方法),PendingList队列首就是引用类中的待定对象。
看看Java层的代码
private static class ReferenceHandler extends Thread {
...
public void run() {
while (true) {
tryHandlePending(true);
}
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
//如果是Cleaner对象,则记录下来,下面做特殊处理
c = r instanceof Cleaner ? (Cleaner) r : null;
//指向PendingList的下一个对象
pending = r.discovered;
r.discovered = null;
} else {
//如果pending为null就先等待,当有对象加入到PendingList中时,jvm会执行notify
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
}
...
// 如果时CLeaner对象,则调用clean方法进行资源回收
if (c != null) {
c.clean();
return true;
}
//将Reference加入到ReferenceQueue,开发者可以通过从ReferenceQueue中poll元素感知到对象被回收的事件。
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
流程比较简单:就是源源的不断从PendingList中提取元素,然后将加入到ReferenceQueue中,开发者可以通过中民意调查从元素探查到对象被回收的事件。
需要的是,对于清洁类型(继承自虚引用)的对象会有额外的处理:在其对象的对象被回收时,会调用清洁方法,该方法主要是创建对应的资源回收,在堆外内存DirectByteBuffer中就是用Cleaner进行堆外内存的回收,这也是虚引用在java中的典型应用。
有参考的实现类里,各自有什么不同。
软引用
public class SoftReference<T> extends Reference<T> {
static private long clock;
private long timestamp;
public SoftReference(T referent) {
super(referent);
this.timestamp = clock;
}
public SoftReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
this.timestamp = clock;
}
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
软件引用很简单,就可以实现一个目标:时钟和时间时钟。时钟是一个动态变量,每次GC时都保留设置成当前时间。时间戳的实现在每次调用获取方法时将其调用为时钟(如果不相同且对象没被回收)。
那这两个领域的作用是……嗯?这和软引用在记忆经典的时候才被回收,有哪些关系呢?
这些还得看JVM的资源才行,因为决定了对象是否需要被回收,都是在GC中实现的。
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{
...
//还记得上文提到过的DiscoveredList吗?refs_lists就是DiscoveredList。
//对于DiscoveredList的处理分为几个阶段,SoftReference的处理就在第一阶段
...
for (uint i = 0; i < _max_num_q; i++) {
process_phase1(refs_lists[i], policy,
is_alive, keep_alive, complete_gc);
}
...
}
//该阶段的主要目的就是当内存足够时,将对应的SoftReference从refs_list中移除。
void
ReferenceProcessor::process_phase1(DiscoveredList& refs_list,
ReferencePolicy* policy,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
// Decide which softly reachable refs should be kept alive.
while (iter.has_next()) {
iter.load_ptrs(DEBUG_ONLY(!discovery_is_atomic() /* allow_null_referent */));
//判断引用的对象是否存活
bool referent_is_dead = (iter.referent() != NULL) && !iter.is_referent_alive();
//如果引用的对象已经不存活了,则会去调用对应的ReferencePolicy判断该对象是不时要被回收
if (referent_is_dead &&
!policy->should_clear_reference(iter.obj(), _soft_ref_timestamp_clock)) {
if (TraceReferenceGC) {
gclog_or_tty->print_cr("Dropping reference (" INTPTR_FORMAT ": %s" ") by policy",
(void *)iter.obj(), iter.obj()->klass()->internal_name());
}
// Remove Reference object from list
iter.remove();
// Make the Reference object active again
iter.make_active();
// keep the referent around
iter.make_referent_alive();
iter.move_to_next();
} else {
iter.next();
}
}
...
}
refs_lists 中举办了本次GC 发现的引用类型(虚、引用、弱引用等),而process_discovered_reflist 方法的作用就是将不被回收的对象从refs_lists 移除掉掉掉的对象,refs_lists 最后的元素全部是需要被回收的元素,最终激发其第一个元素引发给参考文献。java#pending字段。
ReferencePolicy一共有4种实现:NeverClearPolicy,AlwaysClearPolicy,LRUCurrentHeapPolicy,LRUMaxHeapPolicy。
因为NeverClearPolicy永远返回false,代表永远不回收SoftReference,在JVM中该类没有被使用,AlwaysClearPolicy则永远返回true在referenceProcessor.hpp#setup方法中可以设置policy为AlwaysClearPolicy,至于什么时候会使用AlwaysClearPolicy,大家有兴趣可以自己研究。
LRUCurrentHeapPolicy和LRUMaxHeapPolicy的should_clear_reference方法则是完全相同的:
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;
}
timestamp_clock就是SoftReference的屏幕字段时钟,java_lang_ref_SoftReference::timestamp(p)对应的是字段时间戳。如果上次GC后有调用SoftReference#get,interval为0,否则为等次GC之间的时间差。
_max_interval 则代表了一个临界值,它的值在 LRUCurrentHeapPolicy 和 LRUMaxHeapPolicy 两种策略之间存在差异。,内涵3000+道高级面试题,可在线随时刷看。
void LRUCurrentHeapPolicy::setup() {
_max_interval = (Universe::get_heap_free_at_last_gc() / M) * SoftRefLRUPolicyMSPerMB;
assert(_max_interval >= 0,"Sanity check");
}
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");
}
一个Soft RefolicyMSPerMB默认为1000,那次的计算方法和上次GC后可用的大小相关,计算方法和(堆大小-上次gc时堆使用大小)有关。
看到这里你就被SoftReference 到底什么时候被SoftReference 应该回收了,它的策略(默认应该是LRUCurrentHeapPolicy),有多少可用知道,SoftReference 上一次调用get方法的时间关系关系。
弱引用
public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent) {
super(referent);
}
public WeakReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
可以看到弱引用在Java层只是继承了引用,没有做任何的改动。那些引用字段是什么时候为null的呢?要搞清楚这个问题我们再看下这个问题的process_discovered_reflist:
size_t
ReferenceProcessor::process_discovered_reflist(
DiscoveredList refs_lists[],
ReferencePolicy* policy,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor)
{
...
//Phase 1:将所有不存活但是还不能被回收的软引用从refs_lists中移除(只有refs_lists为软引用的时候,这里policy才不为null)
if (policy != NULL) {
if (mt_processing) {
RefProcPhase1Task phase1(*this, refs_lists, policy, true /*marks_oops_alive*/);
task_executor->execute(phase1);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase1(refs_lists[i], policy,
is_alive, keep_alive, complete_gc);
}
}
} else { // policy == NULL
assert(refs_lists != _discoveredSoftRefs,
"Policy must be specified for soft references.");
}
// Phase 2:
// 移除所有指向对象还存活的引用
if (mt_processing) {
RefProcPhase2Task phase2(*this, refs_lists, !discovery_is_atomic() /*marks_oops_alive*/);
task_executor->execute(phase2);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase2(refs_lists[i], is_alive, keep_alive, complete_gc);
}
}
// Phase 3:
// 根据clear_referent的值决定是否将不存活对象回收
if (mt_processing) {
RefProcPhase3Task phase3(*this, refs_lists, clear_referent, true /*marks_oops_alive*/);
task_executor->execute(phase3);
} else {
for (uint i = 0; i < _max_num_q; i++) {
process_phase3(refs_lists[i], clear_referent,
is_alive, keep_alive, complete_gc);
}
}
return total_list_count;
}
void
ReferenceProcessor::process_phase3(DiscoveredList& refs_list,
bool clear_referent,
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc) {
ResourceMark rm;
DiscoveredListIterator iter(refs_list, keep_alive, is_alive);
while (iter.has_next()) {
iter.update_discovered();
iter.load_ptrs(DEBUG_ONLY(false /* allow_null_referent */));
if (clear_referent) {
// NULL out referent pointer
//将Reference的referent字段置为null,之后会被GC回收
iter.clear_referent();
} else {
// keep the referent around
//标记引用的对象为存活,该对象在这次GC将不会被回收
iter.make_referent_alive();
}
...
}
...
}
不管是弱引用还是其他引用类型,将域引用置空的操作都发生在process_phase3中,而具体行为是由clear_referent的值决定的。而clear_referent的值和引用类型相关。
ReferenceProcessorStats ReferenceProcessor::process_discovered_references(
BoolObjectClosure* is_alive,
OopClosure* keep_alive,
VoidClosure* complete_gc,
AbstractRefProcTaskExecutor* task_executor,
GCTimer* gc_timer) {
NOT_PRODUCT(verify_ok_to_handle_reflists());
...
//process_discovered_reflist方法的第3个字段就是clear_referent
// Soft references
size_t soft_count = 0;
{
GCTraceTime tt("SoftReference", trace_time, false, gc_timer);
soft_count =
process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
is_alive, keep_alive, complete_gc, task_executor);
}
update_soft_ref_master_clock();
// Weak references
size_t weak_count = 0;
{
GCTraceTime tt("WeakReference", trace_time, false, gc_timer);
weak_count =
process_discovered_reflist(_discoveredWeakRefs, NULL, true,
is_alive, keep_alive, complete_gc, task_executor);
}
// Final references
size_t final_count = 0;
{
GCTraceTime tt("FinalReference", trace_time, false, gc_timer);
final_count =
process_discovered_reflist(_discoveredFinalRefs, NULL, false,
is_alive, keep_alive, complete_gc, task_executor);
}
// Phantom references
size_t phantom_count = 0;
{
GCTraceTime tt("PhantomReference", trace_time, false, gc_timer);
phantom_count =
process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
is_alive, keep_alive, complete_gc, task_executor);
}
...
}
可以看到一些软引用和我们可以明确引用的可能性,我们的目的都是真实的,对象不符合之后,引用之后就会被置为null,对象就会被回收(对于软引用)说,如果内存访问的话,在第一阶段,相关的引用会从refs_list中被移除,到第三阶段时refs_list为空集合)。,内涵3000+道高级面试题,可在线随时刷看。
但对于最终和 Phantom,clear_referent 被对象引用是错误的,说明这两种引用类型的引用,如果没有其他额外处理,只要引用对象还嘉,那引用的对象就不会被引用回收的。最后的参考文献和对象是否已经完成了最后确定方法,不在本文分析范围之内,我们接下来去看看幻影参考文献。
幻影参考
public class PhantomReference<T> extends Reference<T> {
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
可以看到虚引用的get方法永远返回null,我们看个demo。
public static void demo() throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> refQueue =new ReferenceQueue<>();
PhantomReference<Object> phanRef =new PhantomReference<>(obj, refQueue);
Object objg = phanRef.get();
//这里拿到的是null
System.out.println(objg);
//让obj变成垃圾
obj=null;
System.gc();
Thread.sleep(3000);
//gc后会将phanRef加入到refQueue中
Reference<? extends Object> phanRefP = refQueue.remove();
//这里输出true
System.out.println(phanRefP==phanRef);
}
从代码中看到,虚引用能够在目标不时选择性得到一个“以上实际上所有继承引用的类这个功能”,需要注意的是 GC 完成后,phanRef.referent 主引导之前创建Object,Object 对象一直没被回收!
这导致这一现象的原因在上一小节已经说了:对于最终参考和幻影参考,明确参考目标的时间错误,说明被这种间接引用类型的对象,如果没有其他额外处理,在GC中是不会被回收的。,内涵3000+道高级面试题,可在线随时刷看。
对于虚引用来说,从refQueue.remove();得到引用对象后,调用强行启动可以引用和对象之间的关系,引用对象可以被GC时清除掉。
结尾
针对文章提出的几个问题,观众分析,我们已经可以回答了:
1.我们经常在网上看到软引用的介绍是:在内存不足的时候才会回收,那内存不足是怎么定义的?为什么才叫内存不足?
软引用会在内存不足时被回收,内存不足的定义和该引用的对象得到时间以及当前的大量内存大小关系,计算公式中的知识也可以给出。
2.网上对于虚引用的介绍是:形同虚设,与其他几种引用不同,虚引用并不会决定对象的生命周期。主要回收对象被垃圾回收的活动。真的是这样吗?
认真的说,虚引用是会影响对象生命周期的,只要虚引用的对象不回收,那其永远不会被回收。所以一般来说,从ReferenceQueue中获得PhantomReference对象后,如果 PhantomReference 对象不会被回收的话(比如被其他 GC ROOT 选择性的对象引用),需要调用清除方法启动 PhantomReference 和其引用对象的引用关系。
3.虚引用Jdk有哪些场景在下呢?
DirectByteBuffer中是用虚引用的子类Cleaner.java来实现堆外内存回收的说,会写一篇文章堆外内存的里里外外。
作者:农夫约翰吉特
https://github.com/farmerjohngit/myblog/issues/10
【051期】阿里面试:为什么 B+ 树更适合作为索引的结构?分析索引原理?
【052期】面试官问:MySQL 中为什么 SQL 查询要使用小表驱动大表?
【053期】面试官问:说说 List 复制深拷贝和浅拷贝的用法与区别?
【054期】面试官问:为什么 Kafka 比其他 MQ 消息队列效率高?
【055期】面试官问:分布式集群环境中如何解决定时任务多次执行的问题?
【056期】谈谈关于 IO 同步、异步、阻塞、非阻塞有什么区别?
【058期】面试官问:Java 线程中如何保证通信,都有哪些方式?