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.

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:

// 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:

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