name: java-memory-leaks description: How to identify and fix Java memory leaks in Android Chrome using LeakCanary traces.
Fixing Java Memory Leaks in Android Chrome
This skill guides the process of identifying, debugging, and fixing Java memory leaks in Android Chrome, typically identified by LeakCanary in instrumentation tests.
Workflow
Confirm the Leak:
- If you suspect a leak, confirm it by adding or running an instrumentation test that exercises the component.
- Ensure the test is non-batched (annotated with
@DoNotBatch). Batched tests can share state and make leak traces ambiguous or hard to reproduce. - Critical: Annotate the test class or method with
@EnableLeakChecksor pass--enable-leak-checksto the test runner. - Run the test and look for LeakCanary detections in the log output.
Analyze Leak Trace:
- Examine the LeakCanary trace from the test failure log.
- Identify the leaking object (usually at the bottom of the trace, marked
with
Leaking: YES). - Identify the GC Root (at the top of the trace).
- Trace the retention path from the GC Root to the leaking object.
- Look for
Leaking: UNKNOWNnodes in between to find where the chain should be broken.
Identify the Break Point:
- Look for references that should be cleared when the activity or component is destroyed.
- Common culprits:
- Observers not unregistered.
- Missing calls to
destroy()on components that hold resources or observers. - Static variables holding references to activities or contexts.
- Inner classes (especially anonymous ones) holding implicit references to the outer class.
- Callbacks passed to long-lived components without lifecycle management.
Apply Fix Patterns:
- Pattern 1: Explicit Unregistration:
- If the leak is due to an observer not being removed, ensure
removeObserver()is called inonDestroy()or equivalent lifecycle teardown. - If the component is not a
DestroyObserver, consider making it one and registering it withActivityLifecycleDispatcher.
- If the leak is due to an observer not being removed, ensure
- Pattern 2: Nullifying References:
- If a field causes leaks after destruction, nullify it in
onDestroy(). - Preferred approach: Mark the field as
@Nullableand add explicit null checks where needed. This is safer and satisfies NullAway without suppressions. - Alternative approach: If marking the field
@Nullablecauses too much "collateral damage" (requiring null checks in many places), you can use@SuppressWarnings("NullAway")on theonDestroy()method to set the field tonullwhile keeping it non-null for the rest of the lifecycle. - Example (Preferred):
private @Nullable CustomTabActivityTabProvider mTabProvider; @Override public void onDestroy() { mTabProvider = null; // Breaks leak trace }
- If a field causes leaks after destruction, nullify it in
- Pattern 3: Avoid Mutation in Observers:
- If cleanup requires mutating state (e.g., calling
removeTab()), ensure this is done by the owner of the component, not by an observer or registrar. Observers should not have side effects that mutate the observed object during teardown.
- If cleanup requires mutating state (e.g., calling
- Pattern 1: Explicit Unregistration:
Verify:
- Rebuild the test target:
autoninja -C out/Debug chrome_public_test_apk. - Run the specific leak test:
out/Debug/bin/run_chrome_public_test_apk --enable-leak-checks -f "YourLeakTest*". - Verify that LeakCanary no longer reports the leak.
- Rebuild the test target:
Best Practices & Gotchas
- Order of Destruction: Nullifying references in
onDestroy()can causeNullPointerExceptions if other components try to use them during their own destruction (e.g., to unregister observers). Ensure that cleanup methods likeunregisterObserverhandle null references gracefully.public void unregisterActivityTabObserver(CustomTabTabObserver observer) { mActivityTabObservers.removeObserver(observer); if (mTabProvider == null) return; // Guard against NPE during destruction Tab activeTab = mTabProvider.getTab(); ... }
Examples
Nullifying References for Cleanup
Before:
public class MyComponent implements DestroyObserver {
private final LongLivedProvider mProvider; // Retains activity
public MyComponent(LongLivedProvider provider) {
mProvider = provider;
}
@Override
public void onDestroy() {
// Provider still holds reference to this component, leaking activity
}
}
After (Preferred):
public class MyComponent implements DestroyObserver {
private @Nullable LongLivedProvider mProvider;
public MyComponent(LongLivedProvider provider) {
mProvider = provider;
}
@Override
public void onDestroy() {
if (mProvider != null) {
mProvider.removeObserver(this); // If applicable
mProvider = null; // Break the leak chain
}
}
}