简体   繁体   中英

May the removal of an unused field cause a garbage collection?

For a library that involves asynchronous operations, I have to keep a reference to an object alive until a certain condition is met.

(I know, that sounds unusual. So here is some context, although it may not strictly be relevant: The object may be considered to be a direct ByteBuffer which is used in JNI operations. The JNI operations will fetch the address of the buffer. At this point, this address is only a "pointer" that is not considered as a reference to the byte buffer. The address may be used asynchronously, later in time. Thus, the buffer has to be prevented from being garbage collected until the JNI operation is finished.)

To achieve this, I implemented a method that is basically equivalent to this:

private static void keepReference(final Object object)
{
    Runnable runnable = new Runnable()
    {
        @SuppressWarnings("unused")
        private Object localObject = object;

        public void run()
        {
            // Do something that does NOT involve the "localObject" ...
            waitUntilCertainCondition();

            // When this is done, the localObject may be garbage collected
        }
    };
    someExecutor.execute(runnable);
}

The idea is to create a Runnable instance that has the required object as a field , throw this runnable into an executor, and let the runnable wait until the condition is met. The executor will keep a reference to the runnable instance until it is finshed. The runnable is supposed to keep a reference to the required object. So only after the condition is met, the runnable will be released by the executor, and thus, the local object will become eligible for garbage collection.

The localObject field is not used in the body of the run() method. May the compiler (or more precisely: the runtime) detect this, and decide to remove this unused reference, and thus allow the object to be garbage collected too early?

(I considered workarounds for this. For example, using the object in a "dummy statement" like logger.log(FINEST, localObject); . But even then, one could not be sure that a "smart" optimizer wouldn't do some inlining and still detect that the object is not really used)


Update : As pointed out in the comments: Whether this can work at all might depend on the exact Executor implementation (although I'd have to analyze this more carefully). In the given case, the executor will be a ThreadPoolExecutor .

This may be one step towards the answer:

The ThreadPoolExecutor has an afterExecute method. One could override this method and then use a sledgehammer of reflection to dive into the Runnable instance that is given there as an argument. Now, one could simply use reflection hacks to walk to this reference, and use runnable.getClass().getDeclaredFields() to fetch the fields (namely, the localObject field), and then fetch the value of this field. And I think that it should not be allowed to observe a value there that is different from the one that it originally had.

Another comment pointed out that the default implementation of afterExecute is empty, but I'm not sure whether this fact can affect the question of whether the field may be removed or not.

Right now, I strongly assume that the field may not be removed. But some definite reference (or at least more convincing arguments) would be nice.


Update 2 : Based on the comments and the answer by Holger , I think that not the removal of "the field itself" may be a problem, but rather the GC of the surrounding Runnable instance. So right now, I assume that one could try something like this:

private static long dummyCounter = 0;
private static Executor executor = new ThreadPoolExecutor(...) {
    @Override
    public void afterExecute(Runnable r, Throwable t) {
        if (r != null) dummyCounter++;
        if (dummyCounter == Long.MAX_VALUE) {
            System.out.println("This will never happen", r);
        }
    }
}

to make sure that the localObject in the runnable really lives as long as it should. But I can hardly remember ever having been forced to write something that screamed "crude hack" as loud as these few lines of code...

If JNI code fetches the address of a direct buffer, it should be the responsibility of the JNI code itself, to hold a reference to the direct buffer object as long as the JNI code holds the pointer, eg using NewGlobalRef and DeleteGlobalRef .

Regarding your specific question, this is addressed directly in JLS §12.6.1. Implementing Finalization :

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable.

Another example of this occurs if the values in an object's fields are stored in registers. … Note that this sort of optimization is only allowed if references are on the stack, not stored in the heap.

(the last sentence matters)

It is illustrated in that chapter by an example not too different to yours. To make things short, the localObject reference within the Runnable instance will keep the life time of the referenced object at least as long as the life time of the Runnable instance.

That said, the critical point here is the actual life time of the Runnable instance. It will be considered definitely alive, ie immune to optimizations, due to the rule specified above, if it is also referred by an object that is immune to optimizations, but even an Executor isn't necessarily a globally visible object.

That said, method inlining is one of the simplest optimizations, after which a JVM would detect that the afterExecute of a ThreadPoolExecutor is a no-op. By the way, the Runnable passed to it is the Runnable passed to execute , but it wouldn't be the same as passed to submit , if you use that method, as (only) in the latter case, it's wrapped in a RunnableFuture .

Note that even the ongoing execution of the run() method does not prevent the collection of the Runnable implementation's instance, as illustrated in “finalize() called on strongly reachable object in Java 8” .

The bottom line is that you will be walking on thin ice when you try to fight the garbage collector. As the first sentence of the cite above states: “ Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. ” Whereas we all may find ourselves being thinking too naively…

As said at the beginning, you may rethink the responsibilities. It's worth noting that when your class has a close() method which has to be invoked to release the resource after all threads have finished their work, this required explicit action is already sufficient to prevent the early collection of the resource (assuming that the method is indeed called at the right point)…

Execution of Runnable in a thread pool is not enough to keep an object from being garbage collected. Even "this" can be collected! See JDK-8055183 .

The following example shows that keepReference does not really keep it. Though the problem does not happen with vanilla JDK (because the compiler is not smart enough), it can be reproduced when a call to ThreadPoolExecutor.afterExecute is commented out. It is absolutely possible optimization, because afterExecute is no-op in the default ThreadPoolExecutor implementation.

import java.lang.ref.WeakReference;
import java.util.concurrent.*;

public class StrangeGC {
    private static final ExecutorService someExecutor =
        Executors.newSingleThreadExecutor();

    private static void keepReference(final Object object) {
        Runnable runnable = new Runnable() {
            @SuppressWarnings("unused")
            private Object localObject = object;

            public void run() {
                WeakReference<?> ref = new WeakReference<>(object);
                if (ThreadLocalRandom.current().nextInt(1024) == 0) {
                    System.gc();
                }
                if (ref.get() == null) {
                    System.out.println("Object is garbage collected");
                    System.exit(0);
                }
            }
        };
        someExecutor.execute(runnable);
    }

    public static void main(String[] args) throws Exception {
        while (true) {
            keepReference(new Object());
        }
    }
}

Your hack with overriding afterExecute will work though.
You've basically invented a kind of Reachability Fence , see JDK-8133348 .

The problem you've faced is known. It will be addressed in Java 9 as a part of JEP 193 . There will be a standard API to explicitly mark objects as reachable: Reference.reachabilityFence(obj) .

Update

Javadoc comments to Reference.reachabilityFence suggest synchronized block as an alternative construction to ensure reachability.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM