3

In Effective Java 3rd edition, on page 50, author has talked about total time an object lasted from its creation to the time it was garbage collected.

On my machine, the time to create a simple AutoCloseable object, to close it using try-with-resources, and to have the garbage collector reclaim it is about 12 ns. Using a finalizer instead increases the time to 550 ns.

How can we calculate such time? Is there some reliable mechanism for calculating this time?

Bagira
  • 2,149
  • 4
  • 26
  • 55
  • Call `System.nanotime` in the constructor and the finalizer? Although adding a finalizer will probably increase the time and bias the results. – assylias Mar 16 '18 at 10:13
  • For information about the finalizer, take a look [here](https://stackoverflow.com/questions/171952/is-there-a-destructor-for-java) – sarkasronie Mar 16 '18 at 10:15
  • @assylias Author has claimed this is the total time without finalizer and with finalizer time increases. I have edited the question to add more details. – Bagira Mar 16 '18 at 10:24
  • Note that there's generally is a huge difference between the time when an object becomes *eligible* for garbage collection vs. the time when it actually gets collected. The former can only be really measured by snapshotting the heap and analyzing reachability of objects. – the8472 Mar 17 '18 at 15:11

2 Answers2

2

The only reliable method I am aware of (I being emphasized here) is in java-9 via the Cleaner API, something like this:

static class MyObject {
    long start;
    public MyObject() {
        start = System.nanoTime();
    }
}

private static void test() {
    MyObject m = new MyObject();

    Cleaner c = Cleaner.create();
    Cleanable clean = c.register(m, () -> {
        // ms from birth to death 
        System.out.println("done" + (System.nanoTime() - m.start) / 1_000_000);
    });
    clean.clean();
    System.out.println(m.hashCode());
}

The documentation for register says:

Runnable to invoke when the object becomes phantom reachable

And my question was really what is phantom reachable after all? (It's a question I still doubt I really understand it)

In java-8 the documentation says (for PhantomReference)

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.

There are good topics here on SO that try to explain why this is so, taking into consideration that PhantomReference#get will always return null, thus not much use not to collect them immediately.

There are also topics here (I'll try to dig them up), where it is shown how easy is to resurrect and Object in the finalize method (by making it strongly reachable again - I think this was not intended by the API in any way to begin with).

In java-9 that sentence in bold is removed, so they are collected.

Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 1
    Your test program is quiet meaningless, as you are invoking `clean()` manually, so you are only measuring the cost of a nested method invocation. (Did you notice that “done” is printed before the hash code?) Measuring the actual cost of garbage collection is impossible with your code, as your cleanup action contains a reference to `m`, so without a manual `clean()`, it would never get garbage collected. Besides that, a `Cleaner` approach is not capable of measuring the overhead the object would have without it. – Holger Mar 16 '18 at 16:52
  • @Holger oh now that makes sense! thank you, but why is `register` present then? How are we supposed to use it, Im so confused. By calling `register(instance, ...)` that instance is going to be strongly reachable, when is it ever going to be phantom referenced? damn it, Im in a total darkness right now – Eugene Mar 19 '18 at 08:23
  • You have to use it the same way, you would use a `PhantomReference` manually, like in [this answer](https://stackoverflow.com/a/49301869/2711488), the cleanup action and the referent may share properties, but the cleanup action must not refer to the object. In your code, it’s quiet simple to capture the value of `m.start` before calling `register`. Note how [the documentation](https://docs.oracle.com/javase/9/docs/api/?java/lang/ref/Cleaner.html) mentions the issue (at “API Note”) though I wouldn’t discourage lambda expressions, just putting them into a `static` method would be enough. – Holger Mar 19 '18 at 08:58
2

Any attempt to track the object’s lifetime is invasive enough to alter the result significantly.

That’s especially true for the AutoCloseable variant, which may be subject to Escape Analysis in the best case, reducing the costs of allocation and deallocation close to zero. Any tracking approach implies creating a global reference which will hinder this optimization.

In practice, the exact time of deallocation is irrelevant for ordinary objects (i.e. those without a special finalize() method). The memory of all unreachable objects will be reclaimed en bloc the next time the memory manager actually needs free memory. So for real life scenarios, there is no sense in trying to measure a single object in isolation.

If you want to measure the costs of allocation and deallocation in a noninvasive way that tries to be closer to a real application’s behavior, you may do the following:

  • Limit the JVM’s heap memory to n
  • Run a test program that allocates and abandons a significant number of the test instances, such, that their required amount of memory is orders of magnitude higher than the heap memory n.
  • measure the total time needed to execute the test program and divide it by the number of objects it created

You know for sure that objects not fitting into the limited heap must have been reclaimed to make room for newer objects. Since this doesn’t apply to the last allocated objects, you know that you have a maximum error matching the number of objects fitting into n. When you followed the recipe and allocated large multiples of that number, you have a rather small error, especially when comparing the numbers reveals something like variant A needing ~12 ns per instance on average and variant B needing 550 ns (as already stated here, these numbers are clearly marked with “on my machine” and not meant to be reproducible exactly).

Depending on the test environment, you may even have to slow down the allocating thread for the variant with finalize(), to allow the finalizer thread to catch up.
That’s a real life issue, when only relying on finalize(), allocating too many resources in a loop can break the program.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • I understand that in real life scenario it does not make much sense but my question was more of my curiosity to know how author might have calculated this time. – Bagira Mar 17 '18 at 16:36
  • @BalkrishanNagpal I’m very sure that the author did like I described, measuring the creation and implicit reclamation for a large quantity of objects, followed by calculating the average time for a single object. – Holger Mar 19 '18 at 07:41