The Composable Memory Leak And Java VM Shut Down Issue

Artem Shevchenko
3 min readNov 28, 2023

--

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

I finished my sprint scope and got a new ticket from a backlog: the strange app crashes when a user interacts with dashboard content. From the ticket activity, I realized that this is a pretty old issue, not reproducible on most devices but is still critical.

A few changes between 2 modes and the app freezes and then crashed

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 crashed because of a memory exception — and every content change added additional lugs

No stack trace or any moment I can google or chatGPT, fix a problem, and move the ticket to the next step.

// 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.
class DashboardFragment : Fragment() {
//...
@Composable
fun Content() {
val state = viewModel.EDIT_MODE.collectAsState()

if (state is EditMode.View) {
ViewModeContent()
} else {
EditModeContent()
}
}

@Composable
fun ViewModeContent() {
MyDashboardCard(editMode = false)
}

@Composable
fun EditModeContent() {
EditModeDecor {
MyDashboardCard(editMode = true)
}
}
}

@Composable
fun MyDashboardCard(editMode: Boolean) {
// do not subscribe here!
}

Hope this post will help you avoid a tricky issue with memory leaks and Java VM shut down!

--

--