Memory leaks
Memory Leaks Deep Dive¶
Overview¶
A memory leak in Android occurs when an object is no longer needed but remains referenced, preventing garbage collection. The result is growing memory usage, eventual OutOfMemoryError, and app crashes. Android-specific leaks often involve holding Context or Activity references in long-lived objects.
Memory Management Basics¶
Garbage Collector & Reachability¶
How GC Works:
1. Mark phase: Find all reachable objects from GC roots
2. Sweep phase: Delete unreachable objects
3. Compact phase: Reorder memory
Unreachable objects = collected
Still referenced objects = kept in memory (even if unused)
GC Roots (What keeps objects alive)¶
โโ Running threads
โโ Stack variables (local scope)
โโ Static references
โโ JNI references
โโ Monitored objects
โโ System classes
Memory Leak Definition¶
Object A no longer needed
But still referenced by Object B
Object B still referenced by active code
Result: A never garbage collected โ LEAK
Common Android Leak Patterns¶
Pattern 1: Static Reference to Activity¶
class MyAnalytics {
companion object {
var activity: Activity? = null // โ LEAK
}
fun trackEvent() {
activity?.let { act ->
// Activity reference never released
}
}
}
// In MainActivity
override fun onCreate() {
MyAnalytics.activity = this
// Activity destroyed, but MyAnalytics.activity still holds reference
}
Leak Chain:
MyAnalytics.activity โ MainActivity (static ref)
โ Views
โ Resources
โ ALL LEAKED
Fix:
companion object {
var context: Context? = null // Use app context instead
}
override fun onCreate() {
MyAnalytics.context = applicationContext // Won't leak
}
Pattern 2: Inner Class Holding Outer Activity¶
class MyActivity : AppCompatActivity() {
inner class MyThread : Thread() { // โ LEAK
override fun run() {
Thread.sleep(60000)
// During sleep, implicit 'this@MyActivity' ref held
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MyThread().start() // Activity held for 60 seconds
}
}
Leak Chain:
MyThread โ (implicit this$0) โ MyActivity โ All resources
Fix:
private static class MyThread extends Thread { // Static = no implicit Activity ref
private final WeakReference<MyActivity> activityRef;
MyThread(MyActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
override fun run() {
val activity = activityRef.get() // May be null if GC'd
if (activity != null) {
// Use activity
}
}
}
Pattern 3: Handler with Delayed Messages¶
class MyActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handler.postDelayed({
// Runnable implicitly holds 'this' Activity reference
updateUI() // This runs 60s later
}, 60000)
}
override fun onDestroy() {
super.onDestroy()
// โ If not removed, Activity leaked for 60s
}
}
Leak Chain:
Handler.MessageQueue โ Message โ Callback (Runnable)
โ (implicit this) โ Activity
Fix:
override fun onDestroy() {
handler.removeCallbacksAndMessages(null) // Remove pending messages
super.onDestroy()
}
// OR use coroutines (auto-cleanup)
viewModel.doWork() // Auto-cancelled when Activity destroyed
Pattern 4: Unregistered Listener/Broadcast Receiver¶
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
eventBus.register(this) // โ If never unregistered
// Activity destroyed but still registered
// EventBus holds reference โ Activity can't be GC'd
}
// โ onDestroy never called, or unregister missing
}
Fix:
override fun onDestroy() {
eventBus.unregister(this)
super.onDestroy()
}
Pattern 5: Long-Lived Singleton Holding Activity Context¶
object MySingleton {
var context: Context? = null // Activity context stored
}
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MySingleton.context = this // โ Activity stored in singleton
// Activity destroyed but singleton still holds reference
}
}
Fix:
object MySingleton {
var context: Context? = null
}
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MySingleton.context = applicationContext // App context - safe
}
}
WeakReference vs SoftReference¶
WeakReference¶
val weakRef = WeakReference<Activity>(activity)
// Usage
val activity = weakRef.get()
if (activity != null) {
activity.doSomething()
}
// If no strong reference elsewhere, GC collects immediately
When to use: - Storing references to Activities - Objects that can be recreated - Short-term caching
SoftReference¶
val softRef = SoftReference<Bitmap>(bitmap)
// Usage
val bitmap = softRef.get()
if (bitmap != null) {
drawBitmap(bitmap)
}
// GC keeps alive unless memory pressure
When to use: - Image caches - Objects expensive to recreate - Long-term caching
Detection Tools¶
LeakCanary (BEST TOOL)¶
Setup:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
Features: - Automatically detects Activity leaks - Shows leak chain in logcat - Provides fix suggestions - No code changes needed
How it works:
1. Watches destroyed activities
2. Forces GC
3. Checks if still in memory
4. If yes: Dumps heap, analyzes leak chain
Android Studio Memory Profiler¶
1. Run app in debuggable mode
2. Open Android Studio โ Profiler
3. Click Memory section
4. Perform rotations, navigate, etc
5. Force garbage collection (GC button)
6. Inspect heap dump
7. Look for retained Activities
Logcat Monitoring¶
adb logcat | grep -i "finalize"
adb logcat | grep -i "OutOfMemory"
adb logcat | grep -i "leak"
Manual Detection (Monkey Testing)¶
// Rotate device multiple times while app running
// Watch memory in Profiler
// Check if memory grows unbounded
Prevention Strategies¶
1. Context Usage¶
// โ
Use application context for singletons/long-lived objects
val context = applicationContext
// โ Don't store Activity context in singletons
companion object {
var activity: Activity? = null // WRONG
}
2. Static Inner Classes with WeakReference¶
// โ
Correct pattern for holding Activity reference
private static class MyTask extends AsyncTask {
private final WeakReference<MyActivity> activityRef;
MyTask(MyActivity activity) {
this.activityRef = new WeakReference<>(activity);
}
protected void onPostExecute(Result result) {
MyActivity activity = activityRef.get();
if (activity != null) {
activity.updateUI(result);
}
}
}
3. Always Unregister Listeners¶
override fun onResume() {
super.onResume()
eventBus.register(this)
}
override fun onPause() {
eventBus.unregister(this)
super.onPause()
}
4. Remove Handler Messages¶
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
5. Use Lifecycle-Aware Components¶
// โ
ViewModel + LiveData (auto lifecycle aware)
viewModel.data.observe(this) { data ->
updateUI(data)
}
// โ Manual subscription (must unsubscribe)
subscription = dataSource.subscribe { data ->
updateUI(data)
}
override fun onDestroy() {
subscription.dispose()
}
6. Clean Up Binding References¶
private var binding: ActivityBinding? = null
override fun onDestroyView() {
binding = null // Null out binding
super.onDestroyView()
}
7. Don't Pass Activity to Threads¶
// โ WRONG
Thread {
val activity: Activity = this@MyActivity
Thread.sleep(60000)
activity.updateUI()
}.start()
// โ
CORRECT: Use WeakReference
val threadSafe = Thread {
val ref = WeakReference(this@MyActivity)
Thread.sleep(60000)
ref.get()?.updateUI()
}.apply { start() }
Production Monitoring¶
Firebase Performance Monitoring¶
val memory = Runtime.getRuntime().let {
(it.totalMemory() - it.freeMemory()) / 1024 / 1024
}
Analytics.log("memory_used_mb", memory.toDouble())
Custom Memory Tracking¶
fun trackMemory(tag: String) {
val runtime = Runtime.getRuntime()
val usedMemory = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024
Log.d(tag, "Memory: $usedMemory MB")
}
// In various lifecycle methods
override fun onPause() {
trackMemory("onPause")
super.onPause()
}
Interview Knowledge¶
Q: What's the difference between OutOfMemoryError and memory leak? A: Memory leak = unused object still referenced (prevents GC) OOM = not enough memory left to allocate new objects Leaks cause OOM if allowed to accumulate
Q: Does assigning null prevent leaks? A: No, if object still referenced elsewhere, it's still a leak
Q: Why not always use WeakReference? A: Weak references can be GC'd at any time Use only when cleanup acceptable For critical objects, hold strong references
Key Takeaways¶
โ Leak = unused object still referenced
โ Common: Static refs, unregistered listeners, stored contexts
โ LeakCanary = best tool for detection
โ Use application context for long-lived objects
โ Always unregister listeners/receivers in onDestroy()
โ Use WeakReference for Activity references
โ Remove Handler messages in onDestroy()
โ Use ViewModel/LiveData instead of manual management