I go with door #2: This is not a real leak.
More specifically, it's just related to garbage collection. Three clues:
The path to GC root ends at FinalizerReference
, which is closely related to GC. It basically handles calling the finalize()
methods on objects that are eligible for GC -- for which there is one here, namely the ViewRootImpl.WindowInputEventReceiver
instance, which extends InputEventReceiver
, which does have a finalize()
method. If this was a "real" memory leak, then the object would not be eligible for GC, and there should be at least one other path to a GC root.
At least in my test case, if I force GC before taking the heap snapshot, then there is only one reference to MainActivity
(whereas there are two if I take the snapshot without doing so). Looks like forcing GC from DDMS actually includes calling all finalizers (most likely by calling FinalizerReference.finalizeAllEnqueued()
which should release all these references.
I could reproduce it in a device with Android 4.4.4, but not in Android L which has a new GC algorithm (admittedly this is at most circumstantial evidence, but it's consistent with the others).
Why does this happen for some activities and not for others? While I cannot say for sure, it's likely that constructing a "more complicated" activity fires GC (simply because it needs to allocate more memory) while a simple one like this one "generally" does not. But this should be variable.
Why does StrictMode think otherwise?
There are extensive comments in StrictMode
about this case, check the source code of decrementExpectedActivityCount()
. Nevertheless, it looks like it's not working exactly as they intended.
// Note: adding 1 here to give some breathing room during
// orientation changes. (shouldn't be necessary, though?)
limit = newExpected + 1;
...
// Quick check.
int actual = InstanceTracker.getInstanceCount(klass);
if (actual <= limit) {
return;
}
// Do a GC and explicit count to double-check.
// This is the work that we are trying to avoid by tracking the object instances
// explicity. Running an explicit GC can be expensive (80ms) and so can walking
// the heap to count instance (30ms). This extra work can make the system feel
// noticeably less responsive during orientation changes when activities are
// being restarted. Granted, it is only a problem when StrictMode is enabled
// but it is annoying.
Runtime.getRuntime().gc();
long instances = VMDebug.countInstancesOfClass(klass, false);
if (instances > limit) {
Throwable tr = new InstanceCountViolation(klass, instances, limit);
onVmPolicyViolation(tr.getMessage(), tr);
}
Update
Actually, I've performed more tests, calling StrictMode.incrementExpectedActivityCount()
using reflection, and I've found a very curious result, which does not change the answer (it's still #2) but I think provides an additional clue. If you increase the number of "expected" instances of the Activity (say, to 4), then the strict mode violation will still occur (claiming 5 instances are present), on every 4th rotation.
From this I'm led to conclude that the call to Runtime.getRuntime().gc()
is what actually releases these finalizable objects, and that code runs only after going beyond the set limit.
What if any action can be taken to fix it?
While it's not 100% foolproof, calling System.gc()
in the Activity's onCreate()
is likely to get this problem to go away (and it did in my tests). However, the specification for Java clearly says that garbage collection cannot be forced (and this is merely a hint) so I'm not sure if I'd trust going with the death penalty, even with this "fix"...
You could possibly combine it with manually increasing the limit for activity instance count by calling reflection. But it seems like a really crude hack:
Method m = StrictMode.class.getMethod("incrementExpectedActivityCount", Class.class);
m.invoke(null, MainActivity.class);
(Note: be sure to do this only once at application startup).