Here is the code before my question. First there is an interface:
public interface SomeAction {
public void doAction();
}
Then there are two classes:
public class SomeSubscriber {
public static int Count;
public SomeSubscriber(SomePublisher publisher) {
publisher.subscribe(this);
}
public SomeAction getAction() {
final SomeSubscriber me = this;
class Action implements SomeAction {
@Override
public void doAction() {
me.doSomething();
}
}
return new Action();
}
// specify what to do just before it is garbage collected
@Override
protected void finalize() throws Throwable {
SomeSubscriber.Count++;
System.out.format("SomeSubscriber count: %s %n", someSubscriber.Count);
}
private void doSomething() {
// TODO: something
}
}
The second class:
public class SomePublisher {
private List<SomeAction> actions = new ArrayList<SomeAction>();
public void subscribe(SomeSubscriber subscriber) {
actions.add(subscriber.getAction());
}
}
This is the code that is used to test the two classes:
public class Test {
//output: "the answer is: 0" for the 1st run after compilation and running attemptCleanUp() first, stays 0 upon repeat run
public static void main (String args []) {
System.out. println("am in main()");
SomePublisher publisher = new SomePublisher();
for (int i = 0; i < 10; i++) {
SomeSubscriber subscriber = new SomeSubscriber(publisher);
subscriber = null;
}
attemptCleanUp();
}
//output: "the answer is: 0" for the 1st run after compilation and running attemptCleanUp() first, rising to 10, 20, 30 ...upon repeat run
public static void answerIsNot0() {
System.out. println("am in answerIsNot0()");
SomePublisher publisher = new SomePublisher();
for (int i = 0; i < 10; i++) {
SomeSubscriber subscriber = new SomeSubscriber(publisher);
subscriber = null;
}
attemptCleanUp();
}
private static void attemptCleanUp() {
threadMessage("Before the gc attempt, the answer is: " + SomeSubscriber.Count);
System.gc();
System.runFinalization();
threadMessage("After the gc attempt, the answer is: " + SomeSubscriber.Count);
}
private static void threadMessage(String message) {
String threadName =
Thread.currentThread().getName();
System.out.format("%s: %s%n",
threadName,
message);
}
}
The printout from main() shows SomeSubscriber.Count value of 1 to 10, while the final line produced The answer is: 0
,like below:
am in main()
main: Before the gc attempt, the answer is: 0
SomeSubscriber count: 1
SomeSubscriber count: 2
SomeSubscriber count: 3
SomeSubscriber count: 4
SomeSubscriber count: 5
SomeSubscriber count: 6
SomeSubscriber count: 7
SomeSubscriber count: 8
SomeSubscriber count: 9
SomeSubscriber count: 10
main: After the gc attempt, the answer is: 0
whereas for answerIsNot0(), the number inside The answer is: <num>
always matches the last number in the SomeSubscriber count:
series.
My questions are: First, do the non-zero values show that the garbage collection indeed happened 10 times? This contradicts with the notion that the 10 subscriber
s are all still referenced by the instances of the local class Action
in the publisher
instance, and therefore not subjected to garbage-collection. Secondly, how has the value of SomeSubscriber.Count changed at the final statement in main (String args []) {} method, but not at the answerIsNot0() method? In another word, why does the same code produce different effect on SomeSubscriber.Count
when placed in main() as opposed to when placed inside answerIsNot0()?
First, there is a significant difference between garbage collection and finalization. Both may have implementation dependent behavior, which is intentionally unspecified, but at least, there’s a guaranty that the virtual machine will perform garbage collection in an attempt to reclaim memory, before an OutOfMemoryError
is thrown.
Finalizers, on the other hand, are not guaranteed to run at all. Technically, finalization can only run after the garbage collector has determined that objects are unreachable and did enqueue them.
This implies that finalize()
methods are not suitable to tell you whether the objects would get garbage collected under normal circumstances, i.e. if the class hadn’t a custom finalize()
method.
Still, you seem to have hit a nail with your test, that raises the issue of reachability:
JLS, §12.6.1. Implementing Finalization… A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
It should be obvious that if there is no variable holding a reference to an object, no “potential continuing computation” can access it. That’s the easiest way to check this. Still, in your example, no potential continuing computation can access the publisher
object, because there is no code performing any access to the variable. This is harder to detect and therefore doesn’t happen until the code gets optimized by the JVM anyway. §12.6.1 states explicitly:
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. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.
See also “Can java finalize an object when it is still in scope?”
This seems to be your issue. In a short-running program that doesn’t get maximally optimized, some unused objects referred by local variables may not get reclaimed immediately, whereas they might get reclaimed earlier with the same code, when it got deeper optimized after multiple runs. It’s not so important whether it is the main
method or another method, it only matters, how often it is invoked or how long it runs (to be considered a hot spot), or more precisely, to which degree it will get optimized during the JVM’s lifetime.
Another issue with your code is related to the following:
JLS, §12.6. Finalization of Class InstancesThe Java programming language does not specify which thread will invoke the finalizer for any given object.
It is important to note that many finalizer threads may be active (this is sometimes needed on large shared memory multiprocessors), and that if a large connected data structure becomes garbage, all of the finalize methods for every object in that data structure could be invoked at the same time, each finalizer invocation running in a different thread.
The Java programming language imposes no ordering on finalize method calls. Finalizers may be called in any order, or even concurrently.
As an example, if a circularly linked group of unfinalized objects becomes unreachable (or finalizer-reachable), then all the objects may become finalizable together. Eventually, the finalizers for these objects may be invoked, in any order, or even concurrently using multiple threads. If the automatic storage manager later finds that the objects are unreachable, then their storage can be reclaimed.
Since you are not taking any measure to ensure thread safe access to the variable SomeSubscriber.Count
, a lot of inconsistencies can show up. Seeing a zero from the main thread even when it has been changed in a finalizer thread, is only one of them. You’ve been lucky that you have seen ascending numbers from one to ten, apparently there was only one finalizer thread in your JRE. Due to the lack of thread safety, you could have seen numbers in arbitrary order, but also some numbers occurring multiple times and others missing, not necessarily arriving at ten after the finalization of ten object at all.