(一)Java 中的引用类型、对象的可达性以及回收处理「终于解决」

(一)Java 中的引用类型、对象的可达性以及回收处理「终于解决」大家应该都知道 Java 中除了强引用类型外还有几个特殊的引用类型:软引用(SoftReference)、弱引用(WeakReference)以及虚引用(PhantomReference),这几个特殊的引用类型在 java.lang.ref 下也有对应的类。大家也应该都知道引入…

大家应该都知道 Java 中除了强引用类型外还有几个特殊的引用类型:软引用(SoftReference)、弱引用(WeakReference)以及虚引用(PhantomReference),这几个特殊的引用类型在 java.lang.ref 下也有对应的类。大家也应该都知道引入这几个特殊的引用类型是和 Java 的对象回收息息相关的,今天这篇文章就是来分析这些引用类型对 Java 中的对象回收有什么影响。

1. 对象可达性分析

分析引用类型之前我们必须要明确的一点是:JVM 并不是采用引用计数的方式来决定对象是否可以被回收,而是通过分析对象的可达性来决定是否回收的。在不考虑特殊引用类型的情况下,对象的可达性可以被简单地分为两类:可达对象和不可达对象,被判断为不可达的对象在垃圾回收阶段就会被回收器回收。

JVM 是如何分析对象的可达性的呢?首先,在分析可达性之前 JVM 会找出那些对虚拟机运行至关重要的对象,没有这些对象整个应用就无法正常运行下去,这些对象被称之为根对象(GC Roots);然后从根对象开始分析所有能被引用到的对象,这些对象便是可达的,而剩下的自然都是不可达对象。

对象可达性
对象可达性

JVM 寻找根对象的过程被称为根节点枚举,寻找的根对象有:

  • 虚拟机栈中引用的对象;

  • 类静态变量引用的对象;

  • 类常量引用的对象;

  • JNI 的 native 方法栈中引用的对象;

  • JNI 中的 global 对象;

凭什么这些对象是根对象,别的对象就不是呢?王侯将相宁有种乎!不是,并没有任何钦定的意思,这都是有原因的。虚拟机栈,也就是每个线程运行时的方法栈,栈中的每个栈帧对应一个方法调用,栈帧中的局部变量表里保存着对应方法的局部变量。试想一下,如果这些正在执行的方法中局部变量引用着的对象被回收了,这个线程还能正常运行吗?native 方法栈也是同理。另外,方法运行时,随时都可能会访问到类中的静态变量以及常量,这些类肯定也是不能被回收的,JNI global 对象也是同理。当然还有一些没有列举到的根对象类型,可以参考 Eclipse 堆内存分析里列举出来的各种根对象类型

2. 对象的五种可达性

前面只是将对象的可达性简单地划分为可达和不可达,那是在没有考虑几种特殊引用类型的情况下,如果把这些特殊引用类型考虑在内的话,对象的可达性多达五种:强可达(Strongly Reachable)、软可达(Softly Reachable)、弱可达(Weakly Reachable)、虚可达(Phantom Reachable)和不可达(Unreachable)。

我们都知道如何去构造不同的引用类型,如下代码:

public class Foo {

  public void bar() {
    // far变量通过强引用形式引用一个新创建的Far对象
    far = new Far();

    // 通过SoftReference软引用一个Far对象
    farSoftReference = new SoftReference<Far>(far);
    farSoftReference = new SoftReference<Far>(far, softReferenceQueue);

    // 通过WeakReference弱引用一个Far对象
    farWeakReference = new WeakReference<Far>(far);
    farWeakReference = new WeakReference<Far>(far, weakReferenceQueue);

    // 通过PhantomReference虚引用一个Far对象
    farPhantomReference = new PhantomReference<Far>(far, phantomReferenceQueue);
  }
}

强引用是 Java 中的默认引用类型,当创建了一个对象并赋值给某个变量(局部变量或成员变量等)时,就建立了一个从该变量到指定对象的一个强引用。特殊引用类型不同于强引用类型,例如当通过 farSoftReference = new SoftReference<Far>(far) 创建了一个 SoftReference 对象时,一方面是建立了一个从 farSoftReference 变量到这个 SoftReference 对象的强引用,另一方面也建立了一个从 farSoftReference 变量到 Far 对象的软引用。在下面的介绍中,当我说对象 A 软引用对象 B 时,我的意思是对象 A 中包含了一个 SoftReference 类型的成员变量,这个成员变量软引用着对象 B,比如下面的代码示例中,我就会说(某个)Tar 对象 软引用着(某个)Foo 对象。

public class Tar {

  private SoftReference<Foo> fooSoftRef;

  public Tar(Foo foo) {
    fooSoftRef = new SoftReference<>(foo);
  }

  ...

}

接下来我会详细地介绍引用类型和对象可达性的关系以及对象的可达性如何影响对象的回收。

2.1 强可达对象

从前面的对象可达性分析介绍中不难得知,一个可达对象可能有多个路径可以回溯到根对象,甚至不同的路径可以回溯到不同的根对象,那么从根对象到可达对象的引用路径就不是唯一的。强可达对象是存在至少一条从根对象到该对象的引用路径,在这条引用路径上所有的引用都是强引用类型。

强可达对象
强可达对象

上图中,对象 C 虽然被对象 B 软引用着,但是因为存在一条 R->A->C 的全是强引用类型的路径,所以对象 C 是强可达的。

JVM 保证,强可达对象永远不会被回收!

2.2 软可达对象

软可达对象,首先对象不是强可达的,即不存在从根对象到该对象上全是强引用类型的引用路径,但是至少存在一条从根对象到该对象的引用路径,路径中除了强引用类型外只有软引用类型,这样的对象便是软可达的。

软可达对象
软可达对象

上图中,对象 A、对象 C 以及对象 D 都是软可达的,虽然对象 C 是被对象 A 强引用着,但是对象 A 本身是软可达的,所以对象 C 也是软可达的。所以你看,一个对象即使被强引用着,也可能是软可达的,比如这里的对象 C;一个对象即使被软引用着,也可能是强可达的,比如强可达那个图里的对象 C。

JVM 是如何对待软可达对象的呢?JVM 会尽量保证在堆内存足够的情况下,不去回收软可达对象,但是这种保证是不可靠的保证,因为很难界定堆内存是否足够,还有当当前堆内存不足时是优先回收软可达对象呢,还是优先进行堆内存的扩容呢等等。但是 JVM 有一点可以保证的是,在 JVM 堆内存达到上限即将抛出 OOM 异常之前会回收所有还没有被回收的软可达对象,如果清理出来的空间依然无法满足内存申请的需求则会真正抛出 OOM 异常。

其实在 openJDK 的虚拟机源码里可以看到,针对软可达对象 HotSpot VM 是有四种回收策略可以选用的:

/** * src/share/vm/memory/referencePolicy.hpp */

class NeverClearPolicy : public ReferencePolicy {
 public:
  virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
    return false;
  }
};

class AlwaysClearPolicy : public ReferencePolicy {
 public:
  virtual bool should_clear_reference(oop p, jlong timestamp_clock) {
    return true;
  }
};

class LRUCurrentHeapPolicy : public ReferencePolicy {
 private:
  jlong _max_interval;

 public:
  LRUCurrentHeapPolicy();

  // Capture state (of-the-VM) information needed to evaluate the policy wfwf
  void setup();
  virtual bool should_clear_reference(oop p, jlong timestamp_clock);
};

class LRUMaxHeapPolicy : public ReferencePolicy {
 private:
  jlong _max_interval;

 public:
  LRUMaxHeapPolicy();

  // Capture state (of-the-VM) information needed to evaluate the policy
  void setup();
  virtual bool should_clear_reference(oop p, jlong timestamp_clock);
};

NeverClearPolicy 就是一直不回收软可达对象除非即将发生 OOM;AlwaysClearPolicy 就是每次 GC 都会回收所有软可达对象;LRUCurrentHeapPolicy 是将当前堆的空闲内存(当前堆容量 – 当前已使用堆内存,单位MB)和 SoftRefLRUPolicyMSPerMB 的乘积做为 SoftReference 对象存活的临界值,GC 时,如果软可达对象对应的引用 SoftReference 对象的存活时间超过了这个临界值,那么这个软可达对象就会被回收,否则不回收;LRUMaxHeapPolicy 是将(堆最大容量 – 当前已使用堆内存,单位MB)和 SoftRefLRUPolicyMSPerMB 的乘积做为 SoftReference 对象存活的临界值,GC 时,如果软可达对象对应的引用 SoftReference 对象的存活时间超过了这个临界值,那么这个软可达对象就会被回收,否则不回收。

SoftRefLRUPolicyMSPerMB 是每 MB 内存对应的时间系数,单位毫秒(MS),HotSpot VM 可以通过参数 -XX:SoftRefLRUPolicyMSPerMB=XXX 指定。当 HotSpot 处于 Client VM 时默认使用 LRUCurrentHeapPolicy,处于 Server VM 时默认使用 LRUMaxHeapPolicy

Android ART 虚拟机中没有那么多回收策略,回收器会在每次 GC 时都会通过一个包含 GC 迭代相关信息的类 IterationGetClearSoftReference() 方法来判断是否回收软可达对象,如果回收就是全部回收,不回收就全都不回收。ART 中有好几个回收器(Mark-Sweep、Concurrent-Copying、Semi-Space等等),各种回收器对待软可达对象可能不太一样,比如 Concurrent-Copying 回收器 和 sticky 的 Mark-Sweep 回收器就完全不回收软可达对象,非 sticky 的 Mark-Sweep 回收器以及 non-generational 的 Semi-Space 回收器每次都会回收,generational 的 Semi-Space 回收器只在 full GC 的时候回收。

从这些回收策略可以看出软可达对象的回收时机是有很大的不确定性的。以前的很多文章介绍 SoftReference 适合用来缓存对象,我个人觉得选择 SoftReference 来缓存对象需要慎重,因为软可达对象回收时机的不确定可会导能致堆中存留过多的软可达对象,进而导致 GC 频率的增加,从而引起程序的卡顿。

2.3 弱可达对象

弱可达对象,对象既不是强可达的也不是软可达的,并且至少存在一条从根对象到该对象的引用路径,路径中的引用类型只能包含强引用、软引用和弱引用类型,且必须包含至少一个弱引用类型。

弱可达对象
弱可达对象

上图中对象 C 和对象 D 都是弱可达的,因为它们都不是强可达或软可达的,并且引用路径中除了弱引用类型外,只包含强引用类型或软引用类型。

JVM 如何处理弱可达对象?弱可达对象会在当次 GC 中被回收器回收。可达性分析是在对象回收之前进行的,当确定了对象的可达性之后,回收器才会以此为依据进行对象的回收。如果在可达性分析时确定了对象是弱可达的话,紧接着的回收阶段,也就是我刚才说的当次 GC,就会将这个对象回收。

但是,有可能你也会发现,弱可达对象的存活时间超出了你的预期,在某次 GC 时你认为已经变成弱可达的对象并没有被回收,把堆内存 dump 出来分析也并没有发现问题,对象的确是弱可达的,为什么会出现这样的问题呢?有两种可能:一、我们知道 JVM 中堆是分代的,可能当时的 WeakReference 对象以及被引用的弱可达对象都在老年代,而 GC 发生在新生代,没有分析到这些对象,自然也不会回收它;二、WeakReference 对象和被引用的对象(referent)在不同的分代里,而 JVM 默认采用的是 ReferenceBasedDiscovery 策略来发现这些弱引用类型,在这个策略中当引用对象和被引用对象在不同的分代里时,就不会去发现并分析这些引用对象,更不会回收被引用的对象。

关于特殊引用类型的发现策略可以参考 openJDK 源码里的相关注释

/** * src/share/vm/memory/referenceProcessor.cpp */

// We mention two of several possible choices here:
// #0: if the reference object is not in the "originating generation"
// (or part of the heap being collected, indicated by our "span"
// we don't treat it specially (i.e. we scan it as we would
// a normal oop, treating its references as strong references).
// This means that references can't be discovered unless their
// referent is also in the same span. This is the simplest,
// most "local" and most conservative approach, albeit one
// that may cause weak references to be enqueued least promptly.
// We call this choice the "ReferenceBasedDiscovery" policy.
// #1: the reference object may be in any generation (span), but if
// the referent is in the generation (span) being currently collected
// then we can discover the reference object, provided
// the object has not already been discovered by
// a different concurrently running collector (as may be the
// case, for instance, if the reference object is in CMS and
// the referent in DefNewGeneration), and provided the processing
// of this reference object by the current collector will
// appear atomic to every other collector in the system.
// (Thus, for instance, a concurrent collector may not
// discover references in other generations even if the
// referent is in its own generation). This policy may,
// in certain cases, enqueue references somewhat sooner than
// might Policy #0 above, but at marginally increased cost
// and complexity in processing these references.
// We call this choice the "RefeferentBasedDiscovery" policy. wfwf
bool ReferenceProcessor::discover_reference(oop obj, ReferenceType rt) {
  ...
}

2.4 虚可达对象

虚可达对象,对象既不是强可达的、也不是软可达的、也不是弱可达的,但是对象的引用路径中存在虚引用类型。

虚可达对象
虚可达对象

上图中对象 C 和对象 D 都是虚可达的。

JVM 如何处理虚可达对象?先来看下 PhantomReference 的 类注释:

/**
 * Phantom reference objects, which are enqueued after the collector
 * determines that their referents may otherwise be reclaimed. Phantom
 * references are most often used for scheduling pre-mortem cleanup actions in
 * a more flexible way than is possible with the Java finalization mechanism.
 * 
 * ...
 *
 * <p> Unlike soft and weak references, phantom references are not
 * automatically cleared by the garbage collector as they are enqueued.  An
 * object that is reachable via phantom references will remain so until all
 * such references are cleared or themselves become unreachable.
 *
 * ...
 *
 */

看第一段注释可能会让人觉得当对象变成虚可达时,它就已经被回收了,将对应的虚引用对象加入到引用队列中用于通知对象已经被回收了,这样可以做一些类似 finalize() 的清理工作。但是实际情况并非如此,后一段注释中说明了虚引用对象并不会主动清理它所引用的对象。软引用对象/弱引用对象在被引用对象变成软可达/弱可达并且回收器决定回收这个被引用对象时会主动断开引用对象和被引用对象之间的连接,其实也就是主动将 Reference 中的成员变量 referent 置为 null,这样回收器才能真正的回收被引用对象。但是虚引用对象并不会这样,它不会主动将成员变量 referent 置为 null,此时被引用对象会一直保留在内存中不会被回收,直到我们手动调用 PhantomReference.clear() 方法去手动清理。

所以当对象变成虚可达时,回收器并不会回收该对象,但是会将 PhantomReference 对象加入到关联的引用队列中来通知用户(开发者)被引用对象到达了虚可达状态,这时可以做一些善后清理工作,然后直到用户(开发者)调用了 PhantomReference.clear() 后被引用对象才会在后续的 GC 阶段被回收。

2.5 不可达对象

不可达对象应该就不用细说了,一言以概之,不存在任何一条路径可以使不可达对象回溯到根对象。

不可达对象会在可达性分析完成后紧接着的 GC 阶段被立即回收。

上述对象的五种可达性是按照由强到弱的顺序来介绍的,对象的可达状态是会变化的,但只会从较强的可达状态向较弱的可达状态变化而不会反过来(如果考虑对象需要执行 finalize() 方法的话,这是存在例外的)。而且上述对象可达性概念并不是我自己编造的,你可以在 java.lang.ref包注释文档Reachability 一节中看到这些可达性的定义。

3. finalize() 方法对对象回收的影响

我们已经介绍了几个特殊引用类型引申出来的几种对象可达性,以及对这几种可达对象的回收处理,但是我们没有考虑到 finalize() 方法对对象回收的影响。如果某个对象的类覆写(override)了 finalize() 方法,那么这个对象在软可达(且虚拟机决定回收这个软可达对象)时、弱可达时、甚至“不可达”时,都可能不会被回收。 现在我们就来看一下 finalize() 方法是如何影响一个对象的回收处理的。

3.1 “F-可达”对象

如果类覆写了 finalize() 方法,在创建该类的对象时,虚拟机同时也会创建一个 FinalReference 类型的对象引用着这个对象。FinalReference 继承自 Reference 类,也是一个特殊引用类型。实际上虚拟机创建的是 FinalReference 的子类对象,也就是 Finalizer 类型的对象,源码如下:

/*
* 虚拟机创建 Java 对象
* src/share/vm/oops/instanceKlass.cpp
*/
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) {
  	// 看这里!调用了 register_finalizer()
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

/*
* 虚拟机调用 Finalizer.register(Object) 方法的地方
* 这里的参数 i 就是需要执行 finalize() 方法的对象
* src/share/vm/oops/instanceKlass.cpp
*/
instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) {
  if (TraceFinalizerRegistration) {
    tty->print("Registered ");
    i->print_value_on(tty);
    tty->print_cr(" (" INTPTR_FORMAT ") as finalizable", (address)i);
  }
  instanceHandle h_i(THREAD, i);
  // Pass the handle as argument, JavaCalls::call expects oop as jobjects
  JavaValue result(T_VOID);
  JavaCallArguments args(h_i);
  methodHandle mh (THREAD, Universe::finalizer_register_method());
  JavaCalls::call(&result, mh, &args, CHECK_NULL);
  return h_i();
}
final class Finalizer extends FinalReference<Object> {

	private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    private static Finalizer unfinalized = null;

    private Finalizer
        next = null,
        prev = null;

    /* Invoked by VM */
    static void register(Object finalizee) {
        // 会在构造方法里调用 add() 方法
        new Finalizer(finalizee);
    }

    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add();
    }

    private void add() {
        // 把自己添加到 unfinalized 表示的链表中
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }

    ...
}

第一部分代码是虚拟机 native 层的代码,可以看到在创建对象时,如果需要执行 finalize() 方法就会调用 Finalizer.register(Object) 方法。

第二部分代码在 Finalizer.register(Object) 这个静态方法中会创建一个 Finalizer 引用对象引用着那个需要执行 finalize() 方法的对象,然后会再调用 Finalizer.add() 这个实例方法,将 Finalizer 对象添加到静态成员变量 unfinalized 代表的一个链表中。

因为有了 Finalizer 这个新的特殊引用类型,我们需要定义一个新的可达状态,当对象到达这个可达状态时,对象的 finalize() 方法将处于等待执行状态。我们暂且将这种新的可达状态叫“F-可达”,这是我自己随便起的名字,因为 Finalizer 或者 FinalReference 都属于 JRE 的内部实现细节,所以在官方文档中并没有定义对应可达状态。

“F-可达”状态对象的可达性要弱于软可达对象和弱可达对象,但是要强于虚可达对象,所以对象可以从软可达状态或者弱可达状态变成“F-可达”状态,这也是为什么官方文档介绍弱可达对象时会有这么一句:

When the weak references to a weakly-reachable object are cleared, the object becomes eligible for finalization.

在后续的文章中会深入到虚拟机实现层面来分析对象的可达性,那时我们就会知道为什么对象的可达性强弱顺序是这样的,现在先记住就行。

“F-可达”对象,对象既不是强可达的,也不是软可达的,也不是弱可达的,但是对象需要执行 finalize() 方法并且该对象的 finalize() 方法还没有执行过。

当对象变成“F-可达”时,该对象对应的 Finalizer 对象会被添加到 Finalizer 类的静态成员变量 private static ReferenceQueue<Object> queue 这个引用队列中,因为从在 Finalizer.register(Object) 方法中创建 Finalizer 对象时关联的引用队列就是它。我们来看 Finalizer 中是如何处理这个引用队列的。

final class Finalizer extends FinalReference<Object> {

	private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
  	
  	...

    private void runFinalizer(JavaLangAccess jla) {
        synchronized (this) {
            if (hasBeenFinalized()) return;
            // 将这个 Finalizer 对象从 unfinalized 链表中移除
            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();
    }

    ...

    private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, "Finalizer");
        }
        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
                }
            }
        }
    }

    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();
    }
}

Finalizer 内部启动了一个守护线程负责从引用队列中取出 Finalizer 对象执行它的 runFinalizer() 方法。一个需要执行 finalize() 方法的对象有且只有一个对应的 Finalizer 对象,当这个 Finalizer 对象从引用队列取出来时,它再也不会出现在引用队列中,所以它的 runFinalizer() 方法以至于对应对象的 finalize() 方法都只会执行一次。另外在 runFinalizer() 方法中当对象的 finalize() 方法执行完后, Finalizer 对象会调用自身的 clear() 方法来清除 Finalizer 对象到 finalizee 对象的引用,此时这个 finalizee 对象会从“F-可达”变成虚可达(如果存在虚引用的话)或不可达。

这是 finalize() 方法影响对象回收的一个方面,既它会阻止对象被回收直到 finalize() 方法被执行完毕。它影响对象回收的另一方面在于这个对象的 finalize() 方法内部到底做了什么。

3.2 对象的“起死回生”

如果你看过《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》这本书,你应该知道对象在执行 finalize() 方法时可能会被重新引用到,进而阻碍对象的回收。

/** * @author 周志明 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("No, I am dead :(");
        }

        //下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("No, I am dead :(");
        }
    }
}

这段代码里,FinalizeEscapeGC 类的 finalize() 方法会将当前执行这个方法的对象赋值给一个静态变量。我们已经知道静态变量属于 GC Root,毫无疑问的强可达对象,所以执行完 finalize() 方法后,对象会从“F-可达”变成强可达对象,而不是变成虚可达或者不可达对象。

这是 finalize() 方法影响对象回收的另一个方面,如果 finalize() 方法实现不当,很有可能会造成内存泄漏,所以尽量尽量不要覆写 finalize() 方法,我们没必要为自己增添这种不必要的风险。当然这种拯救对象的方法只能使用一次,因为我们已经知道对象的 finalize() 方法只会被执行一次。

3.3 finalize() 方法与虚可达

“F-可达”对象的可达性要强于虚可达对象,所以当一个需要执行 finalize() 方法的对象到达虚可达对象时,说明它的 finalize() 方法一定已经执行过了,所以我们可能需要对虚可达对象重新定义:虚可达对象,对象既不是强可达的、也不是软可达的、也不是弱可达的、也不是“F-可达”的,但是对象的引用路径中存在虚引用类型。

虚可达对象是最接近不可达对象的,也就是说它是最接近被回收的,但是它–相比于其他非强可达的对象–也是最容易因使用不当而引起内存泄漏的。其他可达状态的对象,软可达、弱可达、“F-可达”对象都会在虚拟机的控制下逐渐地往更弱的可达状态前进。比如虚拟机会根据软引用回收策略自动将 SoftReferent 对象和软可达对象之间的引用清除,使软可达对象变得弱可达或“F-可达”甚至不可达,对弱可达对象和“F-可达”对象虚拟机都会有这样的自动清除引用关系的处理,但对虚可达对象没有,只有在程序中主动调用 PhantomReference.clear() 方法才会将它们的引用关系清除,否则虚可达对象会一直停留在内存中不会被回收。

总结

由 Java 的几个特殊引用类型:SoftReferenceWeakReferenceFinalReferencePhantomReference引出的几种对象可达性以及它们是如何影响对象的回收暂时就介绍到这里。不知道你们是否想过,为什么对象的可达性由强到弱是软可达->弱可达->“F-可达”->虚可达,虚拟机内部到底是如何处理这些特殊引用类型的,以及虚拟机到底是如何实现可达性分析的。当我写这篇文章的时候,这些问题不断浮现在我脑海,为了弄找到答案,为了尽量输出正确、不误导读者的信息,我也在 JVM 的源码里进行了些许探索。后续有机会的话我会从虚拟机实现的角度来对对象的可达性分析以及引用类型的处理等进行介绍。

文章中引用的源码都是 openJDK 项目 jdk8u 版本的,源码链接如下:

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13812.html

(0)

相关推荐

  • Python中的字符串替换函数:.str.replace

    Python中的字符串替换函数:.str.replace字符串替换函数是指一种用于替换一个字符串中的部分内容的函数,即在原字符串中找到指定的子串,将其替换成指定的新字符串,并返回替换后的新字符串。在Python中,可以使用.str.replace函数来实现字符串的替换。.str.replace函数可以用于文本处理、数据清洗和文本挖掘等领域,可以方便地对文本数据进行清洗和解析。

    2024-03-21
    32
  • openGauss SQL引擎(下)——查询优化[通俗易懂]

    openGauss SQL引擎(下)——查询优化[通俗易懂]上一篇[openGauss SQL引擎 (上)]中我们介绍了SQL引擎概览、SQL解析以及查询优化器的优势和优化技术的分类,本文将详细介绍查询优化的相关内容。 (一)查询重写 查询重写利用已有语句特…

    2023-04-13
    106
  • 查看手机 ip 地址的方法

    查看手机 ip 地址的方法文章浏览阅读1.3w次,点赞2次,收藏17次。每次想知道设备的IP地址的时候都得「设置」-「关于手机」-「状态信息」-「IP地址」很烦对不对?通过adb可以方便地查看。命令:adbshellifconfig|grepMask输出示例:ine

    2023-11-08
    77
  • 优雅地处理上层目录,利用os.pardir实现目录跳转

    优雅地处理上层目录,利用os.pardir实现目录跳转在Python的文件路径操作中,有时需要处理文件路径中的上层目录,例如访问某个文件的上一级目录或者匹配跨越多个父级目录的文件。这时候可以使用os.pardir来表示上层目录,通过添加或者删除上层目录来完成目录跳转。

    2023-12-22
    65
  • SQLServer自增id日常用法[通俗易懂]

    SQLServer自增id日常用法[通俗易懂]在SQLServer数据库中某一个表要设置自增字段,字段类型必须为数值类型,比如decimal、int、numeric、smallint、bigint、tinyint等类型,需要注意的是decima…

    2023-04-09
    110
  • KB3122947(KB3033929)

    KB3122947(KB3033929)

    2023-10-03
    84
  • Python中的字符串大写转换方法

    Python中的字符串大写转换方法在Python中,字符串是一种非常重要的数据类型,字符串的处理对于文本处理、字符串处理等方面非常重要。其中的一个方法就是将字符串中的字母全部转换为大写形式,这对于处理某些场景,例如文本匹配、数据清洗等会非常有用。在本文中我们将介绍Python中的字符串大写转换方法,让读者了解其用法,以提高代码的可读性和可维护性。

    2024-03-03
    37
  • 开源的自动化测试平台_java 自动化测试工具

    开源的自动化测试平台_java 自动化测试工具LuckyFrame测试平台是一款免费开源的测试平台,最大的特点是全纬度覆盖了接口自动化、WEB UI自动化、APP自动化,并且支持分布式测试,

    2023-07-04
    88

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注