Efficient memory management is crucial for building smooth and responsive Android applications. One of the key players in this process is the Garbage Collector (GC). But how does GC work, and why can it sometimes cause our app to lag? Letβs break it down with simple examples and diagrams.
Memory Allocation Example
Imagine our app has a total memory limit of 50MB. We can visualize this as a grid with 10 columns and 5 rows, where each cell represents 1MB.
Diagram 1: Initial Memory Allocation
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Suppose we launch our application and it takes 5MB. Then, we open Activity 1 (10MB) and Activity 2 (10MB):
Application (5MB) + Activity 1 (10MB) + Activity 2 (10MB) = 25MB
Killing Activities and Memory Usage
Now, letβs say we kill Activity 1 and Activity 2 and open Activity 3 (10MB):
Application (5MB) + Activity 3 (10MB) = 15MB
But, the memory used by Activity 1 and Activity 2 (20MB) is still not immediately freed. This is where the Garbage Collector comes in.
Diagram 2: Memory After Killing Activities
A | A | A | A | A | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 |
2 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | 3 |
3 | 3 | 3 | 3 | 3 | - | - | - | - | - |
- | - | - | - | - | - | - | - | - | - |
A = App, 1 = Activity 1, 2 = Activity 2, 3 = Activity 3
How Garbage Collector (GC) Works
The GC periodically checks memory blocks to see if they are still in use. If not, it frees them up. In our example, the 20MB used by the killed activities is now unnecessary, so GC should remove it.
Diagram 3: After GC Cleans Up
A | A | A | A | A | - | - | - | - | - |
- | - | - | - | - | 3 | 3 | 3 | 3 | 3 |
3 | 3 | 3 | 3 | 3 | - | - | - | - | - |
- | - | - | - | - | - | - | - | - | - |
- | - | - | - | - | - | - | - | - | - |
Why Does an Android App Lag?
Android apps are expected to refresh the UI every 16ms (to achieve 60 frames per second). The main thread (UI thread) is responsible for drawing the UI and handling user interactions.
If a task (like onCreate() or GC) takes too long (say, 26ms), the main thread is blocked, and the UI cannot update in time. This causes dropped frames and visible lag.
Diagram 4: Frame Timing and Dropped Frames
Time: |----16ms----|----16ms----|----16ms----|----16ms----|
Draw: | Draw | Draw | Draw | Draw |
If a task takes 26ms:
Time: |----16ms----|------26ms------|----16ms----|
Draw: | Draw | Dropped Frame | Draw |
Main Reasons for App Lag
-
Frequent GC Runs:
When our app uses a lot of memory, GC runs more often, sometimes on the main thread, causing UI freezes. -
Heavy Work on Main Thread:
Long-running tasks (like network calls or heavy computations) block the main thread, delaying UI updates.
How to Avoid Lag
- Move heavy tasks to background threads using
AsyncTask
,Handler
,ExecutorService
, or Kotlin coroutines. - Optimize memory usage to reduce GC frequency.
- Profile our app to find and fix memory leaks.