In a hunt for a memory-leak in my app I chased down a behaviour I can't understand. I allocate a large memory block, but it doesn't get garbage-collected resulting in a OOM, unless I explicit null the reference in onDestroy.
In this example I have two almost identical activities that switch between each others. Both have a single button. On pressing the button MainActivity starts OOMActivity and OOMActivity returns by calling finish(). After pressing the buttons a few times, Android throws a OOMException.
If i add the the onDestroy to OOMActivity and explicit null the reference to the memory chunk, I can see in the log that the memory is correctly freed.
Why doesn't the memory get freed automatically without the nulling?
MainActivity:
package com.example.oom;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class MainActivity extends Activity implements OnClickListener {
private int buttonId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.gc();
Button OOMButton = new Button(this);
OOMButton.setText("OOM");
buttonId = OOMButton.getId();
setContentView(OOMButton);
OOMButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == buttonId) {
Intent leakIntent = new Intent(this, OOMActivity.class);
startActivity(leakIntent);
}
}
}
OOMActivity:
public class OOMActivity extends Activity implements OnClickListener {
private static final int WASTE_SIZE = 20000000;
private byte[] waste;
private int buttonId;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button BackButton = new Button(this);
BackButton.setText("Back");
buttonId = BackButton.getId();
setContentView(BackButton);
BackButton.setOnClickListener(this);
waste = new byte[WASTE_SIZE];
}
public void onClick(View view) {
if (view.getId() == buttonId) {
finish();
}
}
}
A few things:
1) You can't judge whether or not your Activity is being leaked just by watching the GC logs; each JVM implementation is free to choose when to garbage-collect objects, even if nothing is referencing them. Note that it IS required to garbage-collect before it throws an OOM error... but if it has enough memory available, it may choose to keep 10 of your activities in memory and then collect them all at once.
2) There may be internal Android structures that keep a reference to your Activity for longer than the lifecycle of the Activity... and the developer has no control over that. For that reason, it's recommended that the Activity not reference any large amounts of data (or if it does, it should explicitly release those references in onDestroy (or even in onPause if you want to be more aggressive).
3) Some JVMs optimize at runtime, such that a chunk of memory that is never written to or accessed is never actually allocated in physical memory. Maybe for that reason, your test is not valid on newer versions of Android. To get around this, you can add a loop that sets some values in the array to random values, and then another loop somewhere else in the code that reads them; this way the JVM is forced to allocate the memory.
Activity destruction does not imply class destruction, just because you don't see the class doesn't mean the OS (Android in this case) lost all references to it and end it. That is why even in the documentations they specify to clean out any handles and objects you no longer need to prevent memory leaks. Cheers.