Chapters

Hide chapters

Android Debugging by Tutorials

First Edition · Android 12 · Kotlin 1.6 · Android Studio Chipmunk (2021.2.1)

Section I: Debugging Basics

Section 1: 8 chapters
Show chapters Hide chapters

10. Profile Memory Usage
Written by Zahidur Rahman Faisal

Memory is a storage space in computers or mobile devices, just like the human brain! This is a place where data or programs are kept on a temporary or permanent basis to be processed.

Any smartphone app like PodPlay keeps occupying your device’s memory as long as it’s running in the foreground or background. It’s important to ensure your app is functional with minimal memory usage and leaves enough room for the Android OS and other apps to operate correctly. The Memory Profiler is a tool built within Android Studio that helps you understand, analyze and optimize your app’s memory usage.

In this chapter, you’ll use the Memory Profiler and learn:

  • The basics of memory management in Android.
  • Tracking memory allocations.
  • About memory leaks and how to prevent them.

Before moving any further, you might want to recall Android’s memory management basics.

Android Memory Management 101

Android is a managed-memory environment. The Android Runtime (ART) and Dalvik Virtual Machine use memory paging and mapping techniques to manage memory for your Android device. Below are the key elements of Android memory management:

  • Memory Allocation: This is the process of reserving memory for your app’s objects and processes.
  • Stack Memory: Android uses this for static memory allocation. Local variables, references to objects and primitive types are common examples of static memory. Each thread has its separate stack, organized in a LIFO order (last in, first out). The unified stack size on Android Runtime for both Java and C++ is around 1MB, which is relatively small compared to Heap memory. StackOverflowError occurs when an app hits its stack memory limit.
  • Heap: Android uses this for dynamic memory allocation. The heap is a piece of memory where the system allocates Java/Kotlin objects in no particular order. Android runtime limits the heap size for each running application to ensure a smooth multitasking experience. There is a variation of the heap size limit among devices and it depends on how much RAM a device has. If your app hits this heap limit and tries to allocate more memory, OutOfMemoryError will occur, and will terminate your app.

This is what the memory allocation looks like for a simple application:

Stack Heap foo() String str Object param main() int = 1 Object obj Memory mem new Object() new Memory() String Pool

  • Garbage Collection: Garbage Collection is an action taken by the system to avoid memory-related issues such as StackOverflowError or OutOfMemoryError. When the system determines that any program or app no longer uses a piece of memory, it frees the memory back to the heap, without user intervention. The system conducts:

    1. Search for data objects that are no longer referenced and you can’t access them.
    2. Reclamation of the memory occupied by those objects.

    This process cleans up and reclaims unused memory.

Why You Should Profile Memory

Garbage collection is a necessary process for memory management, but if it happens too often, it can negatively impact your app’s performance. The system has to pause the app’s code to let the Garbage Collector do its job. Normally, this process can be quite fast and imperceivable from the user’s perspective.

If you’ve coded your app in a way that’s not very memory efficient and allocates memory faster than the system can collect it, you’ll notice your app is sluggish and skips frames.

In such cases, your code flow may force garbage collection events more often or make them last longer than usual. That slows down the rest of the system. Eventually, the system might kill your app process to reclaim the memory and maintain a functional multitasking environment.

You should profile your app memory to avoid these problems, and that’s where the Memory Profiler comes in handy!

Overview of Memory Profiler

Android Profiler is a combination of various tools that provide real-time information about your app, such as memory allocation and resource usage.

The Memory Profiler is a component of the Android Profiler. With Memory Profiler, you can:

  • View the real-time status of allocated objects and garbage collection events on a timeline.
  • Initiate garbage collection events.
  • Capture heap dumps.
  • Record memory allocations.
  • View the stack trace for each allocation.

And more…

Running Memory Profiler

  1. Open the Podplay starter project and run the app on an emulator or connected device using API level 26 or higher.

  2. Select View ▸ Tool Windows ▸ Profiler from the menu bar.

  3. This will start a new profiling session for PodPlay from the current time. Select MEMORY from the right pane as highlighted below:

This will open the actual Memory Profiler toolset from Android Profiler. Take a minute and have an overview of each of the highlighted sections as numbered below:

  1. The process and device that you are currently profiling using Memory Profiler. You can see your app’s package name here.

  2. The Sessions pane shows your current session, if any. This pane can save Profiler data as sessions until you quit Android Studio. The sessions pane allows you to:

  • Start a new profiling session.
  • Stop adding data to the current session.
  • Import a trace exported from any previous session.
  1. The zoom-in/out buttons control how much of the memory timeline to view or jump to the real-time updates.

  2. The event timeline shows user inputs or actions such as volume changes or screen rotations performed while profiling the app.

  3. The memory graph displays memory that each category uses in different colors, i.e., Java, Native, Graphics, etc. The horizontal axis represents the passage of elapsed time since you started profiling. You can see the number of allocated objects using the numbers on the vertical axis.

  4. Options to record memory allocations or capture heap dumps. You’ll know how to use these features soon. The options in this section can vary depending on your device’s or emulator’s API version.

Memory Count

To see overall memory usage by PodPlay at any point since the app launched, put the cursor above the event timeline. You’ll see the memory count of your app segmented into several categories as follows:

  • Java: Memory from objects that Java/Kotlin code has allocated.
  • Native: Allocated memory from C/C++ code objects.
  • Graphics: Memory to display pixels to the screen. This is a memory shared with the CPU, not dedicated GPU memory.
  • Stack: Memory used by both native and Java stacks in your app. When your app invokes a method, it creates a block in the stack memory to hold local primitive values and references to other objects in this method. This normally relates to how many threads your app is running.
  • Code: Memory used for code and resources such as dex bytecode, .so libraries and fonts.
  • Others: Memory that the system doesn’t know how to categorize.
  • Allocated: The number of Java/Kotlin objects your app has allocated. Objects allocated by C/C++ code aren’t counted here.

Note: Even if you’re not using C/C++ in your app, you might see some native memory used because the Android framework uses native memory to handle various tasks, such as loading image assets, though the SDK methods for those were in Java or Kotlin.

The numbers you see are based on all the private memory chunks that your app has consumed. This count doesn’t include memories shared with the system or other apps.

Tracking Memory Allocations

The memory allocations panel helps you see each object and JNI reference in your memory:

  • The types of objects allocated and how much space they’re using.
  • The stack trace of each object allocation, including the information about their corresponding threads.
  • When the objects were deallocated, this is only available for devices with Android 8.0 or higher.

Recording Memory Allocation

You’ll now see how to analyze when you use PodPlay and objects the app creates in memory. Open the Memory Profiler, select Record Java / Kotlin allocations and click Record:

Then enter something into the search bar in PodcastActivity. That’ll show a list of podcast channels based on your search query. Tap any item from the list, it will display details about the selected channel in PodcastDetailsFragment as follows:

The Memory Profiler recorded all your actions and memory allocations. Your recorded session will look like this:

This might draw your attention to two focus areas:

  1. The events you performed logged there, such as keyboard inputs or taps. That’s how an event timeline displays user events in real time for a session.
  2. A detailed table listing memory allocations underneath the event timeline.

Now it’s time to learn more about the memory allocations table.

Memory Allocation Table

The table displays a list of allocated objects, grouped by class name and sorted by their heap count as it’s shown below:

The Class Name column explains itself. Look at the other columns in this table:

  • Allocations: Total number of objects currently allocated in memory for a specific class type in your session.

  • Deallocations: Number of objects of that class that’s already deallocated or garbage-collected until now.

  • Total Count: Number of total objects remaining for that class in the session.

    Total Count = Allocations - Deallocations

  • Shallow Size: The total size in bytes of all instances of the class in the memory.

    Shallow Size = Memory consumed by one object * Number of objects.

  • Shallow Size Change: Difference in Shallow Size since the last garbage collection or memory deallocation happened.

It’s easy to browse the list to find objects with unusually large heap counts, for example, Bitmaps. Click the Shallow Size column header to sort the list by largest memory allocating classes.

Note: Click any column header in the list to sort results by that field.

To find known classes quickly, enter a class or package name in the search field:

If your search query is case-sensitive, check the box next to Match Case

Check the box next to Regex if you want to use regular expressions.

You can also search by package or method name if you select Arrange by package or Arrange by callstack from the dropdown menu on the left of the search field.

The PodPlay app uses the Glide image loading library to download images, also known as Bitmaps, from remote servers and handle the image loading on separate threads. That keeps PodPlay’s Main thread free and always responsive to user interactions. You might be wondering what that looks like in memory.

Switch to the Visualization tab in the highlighted area below, and you’ll be amazed to see the organization of all the Bitmap-related threads in the memory:

The above image shows the Main Thread and other threads from Glide to load Bitmaps with their method call-stack.

To see even more details from the list, switch back to the Table tab and click a class name, for example, BitmapDrawable. A new pane will open below the list as shown here:

This pane is in two main segments:

  1. Instance List: A list of all the instances of your selected class, in this case - BitmapDrawable, including their allocation/deallocation time and the memory each instance consumed, namely, Shallow Size.

  2. Instance Details: This section shows the allocation of that instance and which thread it’s in. This is the call stack for that object. From the above image, you can see the BitmapDrawable being fetched with a get() call from the Glide library, all from the main thread.

Forcing Garbage Collection

At this point, you might want to see how garbage collection impacts memory allocation. To force a garbage collection, click the Trash icon highlighted below:

That’ll free up some memory. It will mark the moment garbage collection occurred in the timeline as shown above. See if you can find the difference in memory allocation using the skills you just gained!

Improving Profiling Performance

To improve performance during profiling, the Memory Profiler samples memory allocations periodically. You can change this by using the Allocation Tracking dropdown beside the Trash icon shown above.

The options available are as follows:

  • Full: This captures all object allocations in memory. A downside of this option is that if you have an app that allocates many objects, you may observe visible slowdowns with your app while profiling.
  • Sampled: Samples object allocations in memory at regular intervals. This option is selected by default as it has less impact on app performance while profiling. Apps that allocate many objects over a short period may still suffer from slowdowns.
  • Off: Stops tracking your app’s memory allocation.

You can end the sessions now by clicking Stop in Memory Profiler.

Memory Churn And Memory Leaks

You just learned to force garbage collection by yourself while using the Memory Profiler. Android Runtime performs garbage collection periodically in a typical scenario, but what if there’s a case that “forces” the system to do it?

Forced garbage collection occurs when the app allocates but also has to deallocate objects in a short period. It can, for example, happen if you allocate heavy objects like Bitmaps in loops. In each iteration, they’ll keep saturating your heaps. The system not only has to allocate a large object but it also has to deallocate it from the previous iteration, so it doesn’t run out of memory, resulting in more garbage collections. This situation is a Memory Churn. Users may notice stuttering or slowdown in the app because of this frequent garbage collection.

A Memory Leak happens when your code allocates memory for objects but never frees that memory or is unable to deallocate it. Over time, the memory allocated for these objects turns into a large, immovable block, forcing the rest of the app to operate in what’s left of the total heap memory. If this continues, eventually, the app can run out of memory and crash.

Label Unreferenced Objects Referenced Objects Unused Objects Memory Leak happens here.

Memory leaks can be huge and obvious, such as when your app is trying to load a high-resolution Bitmap that’s larger than the available memory. Some memory leaks can be hard to find, so the user will only notice the app lagging over time. Capturing heap dumps and analyzing them can help to figure out such memory leaks.

Identifying and Improving Memory Performance: Detecting Memory Leaks

You can analyze the current state of the memory by performing a heap dump. It shows which objects in your app are using memory when you capture them. After a user session, a heap dump can help identify memory leaks by showing objects still in memory that should no longer be there.

A captured heap dump shows you the following:

  • The types of objects your app has allocated, and how many of each type.
  • Memory used by each object.
  • Where references to each object are being held in your code.
  • The call stack for where an object was allocated.

Note: You can capture heap dump while recording allocations and get a referent call stack with Android 7.1 or higher only.

Capturing a Heap Dump

To capture a heap dump, perform these actions as you’ve previously completed:

  1. Run the app on a device.
  2. Run Android Profiler and, in the type dropdown, switch to the Memory section.
  3. Enter something into the search bar.
  4. Select any item from the podcast channel’s list and go to PodcastDetailsFragment to see details.
  5. Tap the Back icon and go back to the list screen.
  6. In the Memory Profiler, select Capture heap dump, then click Record.

While dumping the heap, you may observe the amount of memory getting increased temporarily. You should expect this because the heap dump occurs in the same process as your app and requires some memory to capture the data.

Below the timeline, you’ll see the heap dump as follows:

Take a moment to understand what each of the components in this panel offers.

The red circle on top displays a timestamp. It shows the elapsed time from the app launch to the moment you captured the heap dump.

Now, look at the numbered areas. You can see the explanation for each of them below:

  1. Classes: Number of different class types in the heap.
  2. Leaks: Number of potential memory leaks; you’ll jump into this soon!
  3. Count: Total number of allocations in the heap.
  4. Native Size: Total amount (in bytes) of native memory used by the object type. You’ll see memory allocation for some Java objects here because Android uses native memory for some framework classes, such as Bitmap.
  5. Shallow Size: Total amount (in bytes) of Java memory used by this object type.
  6. Retained Size: Total size (in bytes) of memory being retained by all instances of this class.

Next, look at the dropdown menus.

The first dropdown menu on the left lets you choose which heap to inspect:

  • View all heaps: Shows data for all the heaps when the system specifies no heap.
  • View app heap: The primary heap where your app allocates memory.
  • View image heap: The heap for the system boot image. This contains classes that the app preloads during boot time. Allocations here never change or go away.
  • View zygote heap: The copy-on-write heap where an app process is forked from in the Android system.

With the dropdown menu in the middle, you can choose how to arrange the allocations:

  • Arrange by class: Groups all allocations based on the class name. This the default selection.
  • Arrange by package: Groups all allocations based on the package name.
  • Arrange by call stack: Groups allocations into their corresponding call stack. This option only works if you’ve captured the heap dump while recording allocations.

The next dropdown menu lets you filter displayed classes based on the below criteria:

  • Show all classes: Displays all the classes, including base classes, regardless of whether it’s initiated from the system or the app.
  • Show activity/fragment Leaks: Displays class names of the Activity or Fragment if it’s creating a memory leak.
  • Show project classes: Displays classes only from the source code in your project.

Saving and Importing a Heap Dump

After you capture a heap dump, the data is visible in the Memory Profiler as long as the profiler is running. You lose the heap dump as soon as you exit the profiling session.

So, before fixing the memory leak, save this heap dump so that you can compare after applying the fix.

To save a heap dump, hover the cursor on the Heap Dump entry in the Sessions pane, and click the save icon as highlighted below:

An Export As dialog will appear. Save the file in your desired location. The saved file will have .hprof extension.

To import the saved file, click Start new profiler session ▸ Load from file…

Then, choose the file from the browser window.

Note: You can also import a .hprof file by dragging it from the file browser into the Memory Profiler panel.

Fixing a Memory Leak

You might be yearning to fix the memory leak you’ve seen during heap dump. Don’t worry, you can easily filter on profiling data that the Memory Profiler thinks might induce memory leaks in your app.

The Memory Profiler allows you to filter:

  • Activity instances that have been destroyed but are still being referenced from somewhere.
  • Fragment instances that have been destroyed or don’t have a valid FragmentManager but are still being referenced.

There are two ways you can see memory leaking Activity or Fragment right away:

  1. By clicking on the number of memory leaks.
  2. By selecting Show activity/fragment Leaks from the dropdown menu.

Click PodcastDetailsFragment from the list. That’ll reveal more detail below about the memory leaking instance as follows:

Looking at the Instance Details section on the right pane indicates that the whole PodcastDetailsFragment instance might be retaining in the memory even after it’s supposed to be destroyed, causing the memory leak!

Now switch to the References section, then check Show nearest GC root only:

That gives you enough clues that there’s something wrong with the episodeListAdapter instance.

Note: To know more about GC Root (Garbage Collection Root), you can refer to Android Memory Profiler: Getting Started.

To figure out what’s wrong, open PodcastDetailsFragment.kt in the IDE and look at line 59 where episodeListAdapter is declared.

Do you know what’s wrong there? episodeListAdapter was declared within the companion object block, possibly a silly programming error. This creates a static episodeListAdapter instance. Even though when you go back to the previous screen, and PodcastDetailsFragment is supposed to be destroyed, the static instance is still being kept in the memory and referencing the class.

Well, now you know the root cause of the memory leak. To fix that, move the episodeListAdapter out of companion object and place it on the top of the class.

Now re-launch the app, and capture a heap dump with the same steps mentioned in that section. You’ll be glad to see that the memory leak’s finally gone!

Memory Profiling: Tips & Tricks

Memory Leak is very common since developers can’t see it happening while developing the app. That’s why memory profiling is crucial.

You should stress your app code and try forcing memory leaks, especially before you release the app. Below are some ways you can provoke memory leaks in your app:

  • To run the app for an extended period before doing a heap dump. The smaller the leak, the longer you’ll need to run the app to find it.
  • Rotate the device multiple times in different activity states. The system recreates the Activity/Fragment during a screen orientation change. If your app has a reference to one of those objects, the system can’t garbage collect it, and it’ll mark it as a memory leak.
  • Switch between your app and another app while using different UI conditions. For example, navigate to the Home screen, then switch back to your app several times.

As a developer, to avoid creating memory leaks and churns, you should be mindful to not:

  • Leak View objects in any situation, they’re memory-heavy!
  • Reference views from outside of the UI thread.
  • Reference a view in an async callback, you can’t free that view until the task is done.
  • Reference views from static objects. Static objects stick around for the lifetime of the application.
  • Put views into collections, that’s very expensive in terms of memory allocation.
  • Allocate objects in inner loops. Do it outside the loop, or redesign to skip allocation in the first place.
  • Allocate objects in onDraw() of any custom views. onDraw() is called on every frame, taking a heavy toll on the heap memory.

Key Points

  • Memory Profiling keeps app performing at its max. Profiling your app at different stages of development can result in finding memory leaks early.
  • Memory Profiler offers a complete toolset to view memory allocations, method stack-trace and memory leaks if any.
  • Capturing and analyzing heap dumps frequently helps to understand memory allocation and improvement points and find memory leaks.

Where to Go From Here?

Though memory management is a complex and vast topic, you learned a lot about memory allocations, garbage collection, and analyzing memory allocation from a heap dump of fixing memory leaks! To learn more on this topic, check out the tutorials and videos below:

In the next chapter, you’ll learn to profile network activity, so good luck on your next adventure.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.