9
public class Test {
    public static void main(String[] args) throws Exception {
        A aObject = new A();

        ReferenceQueue<A> queue = new ReferenceQueue<>();
        PhantomReference<A> weak = new PhantomReference<>(aObject, queue);

        aObject = null;
        System.gc();

        TimeUnit.SECONDS.sleep(1);

        System.out.println(queue.poll());
    }
}

class A{
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        super.finalize();

        System.out.println("finalize");
    }
}

The result is:

finalize
null

but if i delete the finalize method in class A, the result is:

java.lang.ref.PhantomReference@5b2c9e5d

so, the result shows that when I override the finalize method, the weak object isn't put into the reference queue, is that because the aObject resurrected? But I don't do anything in the finalize method

Tom Tresansky
  • 19,364
  • 17
  • 93
  • 129
gesanri
  • 237
  • 1
  • 3
  • 10
  • Some additional information: uncommenting either `super.finalize()` or `System.out.println(...)` does not change the behaviour. Uncommenting the whole body of method leads to the expected behaviour. Placing both `super.finalize()` and `System.out.println(...)` in an `if (false) { ... }` will also result in the expected behaviour, whereas placing both statement in an `if (true) { ... }` will also result in the unexpected behaviour. Tested with Oracle java 1.8.0_151. Maybe this is somewhat useful. – Turing85 Jan 09 '18 at 12:20
  • Are you sure that this not caused by an entropy in a multi-threaded environment? Does running this test repeatedly in a loop always produce the same result? – M. Prokhorov Jan 09 '18 at 12:54
  • @M.Prokhorov just did some testing. Repeating the code in a loop does not change the outcome (I repeated each constellation 100 times). The application is definitively single-threaded. – Turing85 Jan 09 '18 at 14:25
  • @Turing85, this contradicts the Javadoc which states that `gc` (and, consequently, the finalizers) run in separate thread to the one which user starts. – M. Prokhorov Jan 09 '18 at 14:27
  • @M.Prokhorov it is single-threaded w.r.t. the user-threads. The JVM, of course, does spawn other threads out of the programmer's control. – Turing85 Jan 09 '18 at 14:29
  • @Turing85, when talking about multithreaded environment, here I'm referrign to exactly this: while the application is single-threaded, the application + GC is not, there are at least two threads here. Just noticed there, can you please remove `sleep` call and try running it again? – M. Prokhorov Jan 09 '18 at 14:38
  • @M.Prokhorov I could edit the question and add all the information I have gathered so far, but I feel this would be like "stealing" the question from OP. I myself are waiting for a response of OP. – Turing85 Jan 09 '18 at 14:52

2 Answers2

4

With a nontrivial finalize, Java knows the object is unreachable* before finalize runs, but it doesn't know the object is still unreachable after finalize.

It has to wait for the object to be deemed unreachable again in another GC cycle before it can enqueue the phantom reference.

*not quite unreachable in the terminology of the java.lang.ref docs, but neither strongly, softly, nor weakly reachable

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • What exactly counts as "nontrivial"? What threw me off was that `public void finalize() { super.finalize(); }` seems to be "too complex", but `public void finalize() { if (false) { super.finalize(); } }` is fine. – Turing85 Jan 09 '18 at 21:19
  • 1
    @Turing85: I believe that's left up to the implementation. The [JLS](https://docs.oracle.com/javase/specs/jls/se9/html/jls-12.html#jls-12.6) only says "For efficiency, an implementation may keep track of classes that do not override the finalize method of class Object, or override it in a trivial way. We encourage implementations to treat such objects as having a finalizer that is not overridden, and to finalize them more efficiently, as described in §12.6.1." – user2357112 Jan 09 '18 at 21:29
  • @Turing85 `public void finalize() { if (false) { super.finalize(); } }` is almost certainly identified as containing dead code by the compiler and ends up being equivalent to the trivial empty override. – Hulk Jan 10 '18 at 10:18
  • @Hulk I know. But the compiler should also recognize that `public void finalize() { super.finalize(); }` is effectively a non-overwritten method. – Turing85 Jan 10 '18 at 10:52
  • @Turing85 a compiler can not remove an explicit method declaration, as that has an impact on the accessibility of that method (the `protected` method `Object.finalize()` can only be accessed by subclasses via `super` invocation, the overridden method can be accessed by all classes within the same package, even if you didn’t use `public`). So there is a compiled method which, of course, must retain the `super.finalize()` invocation. In contrast, removing `if(false) { …}` statements at compile-time has no impact (other than making `finalize()` a trivial empty method). – Holger Jan 31 '18 at 11:52
3

Very interesting observation. Here is what is happening:

When class has non-trivial (non empty in OP case) finalize method the JVM will create a java.lang.ref.Finalizer (which is a subclass of Reference) object and point it to our referent, in this case A object. This in turn will prevent PhantomReference to enqueue it since A is already referenced by Finalizer.

This was observer by me in debugger using Java 1.8 and also described in detail here.

The observation from @Turing85 is expected since when we remove all statements inside finalize method it becomes trivial and behaves just like any class without finalize method and wont be referenced by Finalizer.

Update:

The question was asked if Finalizer will clear its reference to A at all. The JVM does clear it on the subsequent GC runs, which in turn finally allows PhantomReference to enqueue A into its reference queue.

For example running the below code with non-trivial finalize method will get non null PhantomReference from its reference queue.

public static void main(String[] args) throws Exception {
    A aObject = new A();

    ReferenceQueue<A> queue = new ReferenceQueue<>();
    PhantomReference<A> pr = new PhantomReference<>(aObject, queue);

    aObject = null;
    System.gc();

    TimeUnit.SECONDS.sleep(1);

    System.gc();

    TimeUnit.SECONDS.sleep(1);

    System.out.println( queue.poll() );
}

Prints:

finalize 
java.lang.ref.PhantomReference@15db9742
tsolakp
  • 5,858
  • 1
  • 22
  • 28
  • A is referenced by Finalizer, then when will the reference be cleared? – gesanri Jan 10 '18 at 02:25
  • The `Finalizer` gets cleared explicitly at the end of [the `runFinalizer` method](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/lang/ref/Finalizer.java#90) though this is usually not even necessary, as no-one is going to keep a reference to the `Finalizer` anyway, so it simply gets garbage collected afterwards. When a `Reference` object is not enqueued and not otherwise reachable, it gets collected like an ordinary Java object and doesn’t count as reference at all. – Holger Jan 31 '18 at 11:32