The Composable Memory Leak And Java VM Shut Down Issue
Artem Shevchenko on 2023-11-28
The Composable Memory Leak And Java VM Shut Down Issue
Can a @Composable function crash your app without any stack traces? Yes, it can. I spent a lot of time researching the issue and here is a short problem and fix description for saving your time!
The problem I met
After completing my sprint tasks, I picked up a new backlog ticket: a rare but critical crash occurring when users interact with dashboard content. From the ticket history, I found that it was an old, hard-to-reproduce issue affecting only specific devices — but still unresolved and important.

On my emulator with API level 29 (Android Q) the issue was reproducible, so the steps were:
- switch between UI modes on the dashboard a few times
- after 3–5 switches app works with lugs
- GC logs in a huge amount produced by Android System trying to free space
- After a few additional content changes, the app started crashing due to a memory exception — each update added more memory load, eventually leading to the failure.
// tons of messages like that Background young concurrent copying GC freed 126695(9014KB) AllocSpace objects, 19(1024KB) LOS objects, 32% free, 20MB/30MB, paused 1.677ms total 213.983ms
So, I separated every piece of logic on the dashboard and finally found the issue.
Short Code Overview
So, on the dashboard screen, we have 2 modes: a View mode and an Edit mode. Every card on the dashboard supports both modes and the DashboardFragment also changes the content based on the current mode.
So, the shortened code of the DashboardFragment:
class DashboardFragment : Fragment() { //... @Composable fun Content() { val state = viewModel.EDIT_MODE.collectAsState() if (state is EditMode.View) { ViewModeContent() } else { EditModeContent() } } @Composable fun ViewModeContent() { MyDashboardCard() } @Composable fun EditModeContent() { EditModeDecor { MyDashboardCard() // the same card - but a different hierarchy so no recomposition here! } } }
And the issue-related code from MyDashboardCard:
@Composable fun MyDashboardCard() { val state = viewModel.EDIT_MODE.collectAsState(); // ... }
So, both components subscribed to the EditMode changes — but the problem is the MyDashboardCard is recreated for every mode change because ViewModeContent() and EditModeContent() have a different hierarchy and recomposition doesn’t work here.
But in the same moment when the root content recomposition itself and changes the ViewModeContent() to the EditModeContent() by removing the first composable card inside the removed hierarchy scheduling the recomposition and somewhere in the deep of Composable internal logic a memory leak happens.
A single-phrase problem description
When the Composable schedules a recomposition and at the same moment it is removed from a UI hierarchy a memory leak can be born.
How to fix a problem
So, I changed the logic in the next way:
- Subscribe for the EditMode changes on the dashboard content level only.
- Pass a current mode as an argument to any dashboard cards.
- Dashboard cards don’t listen for the mode changes so they won’t schedule recomposition when they are removed from the UI.