CN

Android Memory Optimization (1) - Getting Started with MAT

Word count: 3.1kReading time: 19 min
2015/04/11
loading

This is the first article in the series on using the MAT tool. This series consists of three articles, detailing how to use MAT to analyze memory issues, whether they are Java application memory issues or Android application memory issues:

  1. Android Memory Optimization (1) - Getting Started with MAT
  2. Android Memory Optimization (2) - Advanced MAT Usage
  3. Android Memory Optimization (3) - Opening Original Bitmap Images in MAT

Technical review — 2026-10-13: The tutorial and historical screenshots are retained. MAT’s heap-analysis concepts remain useful, while the Eclipse/DDMS capture workflow is obsolete. Installation requirements now reference MAT 1.17.0 (2026-06-10; Java 21+ for the standalone tool). The ART, Bitmap, retained-size, and reference explanations have been corrected.

Start with a Java/Kotlin heap dump in Android Studio Profiler, then export to MAT when its analysis is useful. LeakCanary can help find Android memory leaks. MAT mainly explains why managed-heap objects remain reachable; process-wide memory and native allocations also require tools such as dumpsys meminfo, native allocation recording, or Perfetto heapprofd on Android 10+. On user builds, heapprofd requires a debuggable or profileable app; it cannot arbitrarily profile other apps.

Introduction to MAT

What is MAT?

MAT (Memory Analyzer Tool) is an Eclipse-based Java heap analyzer. It helps us find memory leaks and reduce memory use by showing how much space objects occupy and which references keep them alive. Its reports give us places to start investigating.

image

MAT is available as an Eclipse plugin or a standalone Eclipse RCP application. The standalone tool does not require a full Eclipse IDE, but it still needs a compatible Java runtime. Whether an Android HPROF needs conversion depends on its format and the analyzer’s support, not on which MAT installation method you chose.

Download and Installation of MAT Tool

The current entry point is Eclipse MAT Downloads. At this review, it offers 1.17.0, with Java 21 as the standalone runtime minimum and separate OS/CPU-architecture downloads. That is a requirement for running MAT, not for the Android app being analyzed.

The download and Eclipse installation screenshots below come from the MAT 1.4 era and illustrate the three installation choices:

image

  • Update Site: For MAT 1.17.0, enter https://download.eclipse.org/mat/1.17.0/update-site/ in Eclipse’s Install New Software dialog to install online.

image

  • Archived Update Site: Install an offline package through the same Eclipse dialog. Each update needs a new package download; the online Update Site can fetch updates directly.
  • Stand-alone Eclipse RCP Applications: Run MAT without installing the Eclipse IDE. This is useful when Android Studio is your main IDE. The Android HPROF conversion step is explained below.

After downloading and installing, you can use MAT for actual operations.

Common Memory and Resource-Management Problems in Android (Java)

Android Memory

Before opening MAT, it helps to know how Android allocates memory and what can keep an object alive. The following examples show what to watch for in code.

Android apps normally run in their own processes, and an app can configure multiple processes; one app does not necessarily mean one runtime instance. Dalvik describes the early runtime. ART is the default from Android 5.0 / API 21, as documented in the ART compatibility guide.

A process has a Java-heap capacity limit, and further allocations can fail with OutOfMemoryError. The system may also reclaim processes under memory pressure. Neither event means the system has diagnosed a memory leak: a legitimate but oversized allocation can cause OOM, while useless retained objects can leak well before that limit. Excess memory usage can pressure the whole system, so its effects are not necessarily confined to the allocating process. See Android memory management.

Common Cases of Improper Memory Usage

Cursor Not Closed After Querying Database

Close a Cursor that you own when finished, including on failure paths. A Cursor can hold resources such as CursorWindow; a small result set is not a reason to skip cleanup. Conversely, do not close a Cursor that another component still owns and uses.

This method demonstrates a query, a null-result policy, and cleanup in finally. The caller supplies resolver, uri, and the required column projection. Queries may involve I/O and should run in an appropriate background context:

1
2
3
4
5
6
7
8
9
10
11
public int queryRowCount(ContentResolver resolver, Uri uri, String[] projection) {
Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null) {
throw new IllegalStateException("Provider returned no cursor");
}
try {
return cursor.getCount();
} finally {
cursor.close();
}
}

Do not hide close failures in an empty catch block or silently interpret a null Cursor as a successful zero-row result. See the Cursor contract.

Not Using Cached convertView When Constructing Adapter

For a ListView backed by a BaseAdapter, rows come from this method:

1
public View getView(int position, View convertView, ViewGroup parent)

ListView asks the adapter for enough Views to fill the visible area. As the list scrolls, a row that has moved off screen can be reused for one entering the screen. It reaches getView() as the second argument, convertView. When no recycled View is available, as during initial creation, this argument is null.
Creating a fresh View every time increases allocation, layout, and GC work. If those objects can be reclaimed, however, it is allocation churn rather than evidence of a leak or unlimited memory growth. A reused View must have its current data and state rebound to avoid showing a previous item’s content.

This BaseAdapter method fragment assumes one item type using simple_list_item_1, with getItem(position) supplying the displayed text:

1
2
3
4
5
6
7
8
9
10
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView row = (TextView) convertView;
if (row == null) {
row = (TextView) LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_list_item_1, parent, false);
}
row.setText(String.valueOf(getItem(position)));
return row;
}

Both new and reused rows are bound. Heterogeneous lists must implement getItemViewType() / getViewTypeCount() correctly instead of casting arbitrary recycled Views to one class. See Adapter.getView(). With RecyclerView, likewise distinguish ViewHolder creation from binding.

Bitmap Cleanup Depends on Version and Ownership

“Always recycle a Bitmap when done” is not a universal rule. Pixel storage changed across releases: native memory through Android 2.3.3 / API 10; the managed heap on Android 3.0–7.1 / API 11–25; native heap again from Android 8.0 / API 26. See Bitmap memory management.

The old recommendation to use recycle() carefully addressed API 10 and earlier. Manual recycling still requires clear ownership and certainty that no View, cache, or task will draw the Bitmap again. Drawing a recycled Bitmap fails. Follow the ownership rules of any image library or reuse pool managing it.

Assigning one variable to null removes only that reference. Other strong references may keep the Bitmap alive; becoming collectible does not guarantee immediate GC or an immediate drop in process RSS. On API 26+, the Java wrapper’s shallow size also does not represent the full pixel allocation.

Releasing References to Objects

These reference relationships are easier to follow with examples. Let’s look at two.

Example A: The old example stored work data in Activity.obj and posted a Handler callback to process it. If the Activity no longer needs that field, let the callback capture a local variable instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final Handler mHandler = new Handler(Looper.getMainLooper());

public void operation() {
final Object taskData = initObj();
mHandler.post(new Runnable() {
@Override
public void run() {
useObj(taskData);
}
});
}

@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}

These are Activity method fragments; initObj() / useObj() stand for the example’s existing business methods. This Handler belongs exclusively to the Activity. post() does not create a background thread: this callback runs on the main thread and must not perform lengthy work there.

The callback retains its data until it finishes, and calling an Activity instance method also retains the Activity. Cleanup here removes pending work from the dedicated Handler. On a shared Handler, cancel by Runnable or token to avoid removing another owner’s tasks. Cancellation does not interrupt a callback already running. Moving data from a field to a local variable shortens one retention path; it is not a general Activity-leak fix. See Handler.

Example B:
Suppose we want to listen to the telephone service in the system to obtain some information (such as signal strength, etc.) on the lock screen (LockScreen), we can define a PhoneStateListener object in the LockScreen and register it to the TelephonyManager service. For the LockScreen object, a LockScreen object is created when the lock screen interface needs to be displayed, and we want the object to become collectible once the interface no longer needs it.

If the registration retains the listener, and the listener references LockScreen through a field or inner class, the screen can stay alive after it disappears. Whether that causes OOM, and which process is affected, depends on the actual reference graph and execution context; a system_process crash is not inevitable.

For the old API, unregister the same listener with listen(listener, PhoneStateListener.LISTEN_NONE). PhoneStateListener was deprecated in API 31; use TelephonyCallback for new code and pair registration with unregisterTelephonyCallback(callback). Individual callbacks have their own permission requirements. See PhoneStateListener and TelephonyManager.

When a longer-lived owner retains an object that is no longer useful, remove that ownership path or subscription. Nulling a variable inside the shorter-lived object alone does not remove incoming references.

Others

Pair cleanup with the lifecycle in which a resource is acquired. For a listener needed only while a screen is visible, onStart() / onStop() can be an appropriate pair; Activity-instance resources follow the instance lifetime. Process termination does not guarantee onDestroy(), so critical user data must not depend on that callback for persistence. See the Activity lifecycle.

Using MAT for Memory Debugging

Obtaining HPROF File

An HPROF snapshot records objects, classes, and references. An ordinary Android heap dump mainly answers which objects are alive at that moment; it usually does not provide the timestamp and full allocation stack for every allocation. Record allocation activity separately when investigating how memory was allocated.

Current Entry Point: Android Studio Profiler

Follow Capture a heap dump: select Analyze Memory Usage (Heap Dump) and run the target as debuggable / complete data, then reproduce and capture. Inspect instances, Retained Size, Fields, and References, and export .hprof from Past Recordings. A profileable app does not automatically provide the same complete heap-dump data.

Capture pauses the app and temporarily needs additional memory. Compare snapshots under consistent scenarios while accounting for GC timing. A large object or a reference path alone does not establish a leak.

Historical Entry Point: Eclipse / DDMS

The screenshot below shows the old workflow. Android Device Monitor was deprecated in Android Studio 3.1 and removed in 3.2; a new installation need not provide these DDMS buttons. Historically, select the process under Devices and click Dump HPROF file. An Eclipse setup with the corresponding integration could convert the file and open it in MAT:

image

Export Format and Conversion

When an analyzer does not support Android-format HPROF, use the documented hprof-conv workflow to produce Java SE HPROF. Input and output must be different files:

1
"/path/to/android-sdk/platform-tools/hprof-conv" android-original.hprof mat-converted.hprof

Replace the example tool path with an existing installation, retain the original, and open mat-converted.hprof in MAT. If the installed SDK lacks the tool, check available versions or continue in Android Studio. Renaming an extension does not convert the format, and an already-compatible dump needs no additional conversion.

Introduction to MAT Main Interface

The screenshots here show the Overview page after importing a dump.

  • Open the converted hprof file:
    image

If the first one is selected, a report will be generated. This is fine.

image

  • Select OverView interface:

    Image

We need to pay attention to the Actions area below

  • Histogram: Lists objects in memory, the number of objects, and size

    image

  • Dominator Tree: Organizes objects by domination, commonly using Retained Heap to locate major retainers. Its edges need not be actual field references.

    image

  • Top Consumers: Lists the largest objects graphically

    image

  • Duplicate Classes: Finds classes with the same name loaded multiple times, useful for investigating class loaders. It does not automatically diagnose all memory leaks. A Leak Suspects report also supplies leads rather than a definitive diagnosis. See Duplicate Classes.

Generally, Histogram and Dominator Tree are the most commonly used.

Introduction to Some Concepts in MAT

To understand MAT’s list information, the concepts of Shallow heap, Retained Heap, and GC Root must be understood.

3.3.1 Shallow heap

Shallow size measures an object’s own managed-heap footprint, excluding referenced objects. Headers, primitive fields, reference fields, runtime layout, and alignment affect it. Object arrays hold references; primitive arrays hold their elements. Java fields are not all references, and Java byte[] / char[] arrays are not native allocations.

A Histogram row’s Shallow Heap aggregates the instances of that class. Sorting is selectable, and the leading types depend on the snapshot rather than always being byte[] or char[]. See MAT’s size definitions.

3.3.2 Retained Heap

The retained set of X contains X and the objects reachable from GC Roots only through X. Retained heap sums the shallow sizes of that set. It describes retention in the current object graph, not all native memory or process RSS.

If A and B are independently reachable from GC Roots and both reference array D, removing A does not make D unreachable. D therefore does not belong to A’s individual retained set, or B’s. This is shared ownership rather than a failure of the metric. If D has no other root path, removing the set {A, B} makes it unreachable. Run a Retained Set query for selected objects; neither summing individual retained sizes nor choosing Group by package automatically gives the precise retained size of the group.

Dominator Tree has a specific rule: X dominates Y only when every path from the roots to Y passes through X. A tree parent is its children’s immediate dominator. In a diamond where only A connects the roots to B and C, and both B and C point to D, A dominates B, C, and D; all three can be its direct children. The graph’s actual references have not been changed, and tree edges are not Java field references. See the dominator definition.

For actual references, right-click and choose List Objects:

  • with outgoing references: objects referenced by the selected object.
  • with incoming references: objects that reference the selected object.

To better understand Retained Heap, an example is cited below for illustration:

Treat objects in memory as nodes in the figure below, and objects reference each other. There is a special node GC Roots here, which is the starting point of the reference chain:
image image

Starting from obj1, the blue nodes in the above figure represent objects that can only be accessed directly or indirectly through obj1. Because obj3 can be accessed via GC Roots, obj3 in the left figure is not a blue node; while in the right figure it is blue because it is already included in the retained set.
So for the left figure, the retained size of obj1 is the sum of shallow sizes of obj1, obj2, obj4;
The retained size of the right figure is the sum of shallow sizes of obj1, obj2, obj3, obj4.
The retained size of obj2 can be calculated in the same way.

GC Root

GC Roots are starting points for reachability analysis, such as active-thread local references, JNI references, or runtime-retained classes. Their types depend on the runtime and dump format; “the JVM itself is a GC Root” is not a useful definition. MAT can also use special root types to retain otherwise-unreachable objects in its analysis, so inspect the reported root type. See MAT GC Roots.

An object without a retaining path may become collectible, but GC determines when collection happens. Unreachability does not mean the memory has immediately returned to the OS. Strong, soft, weak, and phantom references must also be distinguished when reading paths.

Some Useful Views in MAT

Thread Overview

Thread Overview shows thread information available in the snapshot. Stacks and local variables depend on the dump; this is not a live thread monitor:
image

Group

Histogram and Dominator Tree offer several grouping options, with different defaults. Group by package makes it easier to find your app’s classes and see which packages use the most memory.
image

Path to GC Root

On a certain entry in the Histogram or Dominator Tree, right-click to view its GC Root Path:
image

Reference types affect retention; their relative strengths do not establish a fixed collection schedule:

  • Strong Reference: An object strongly reachable from GC Roots is not garbage. Removing just one strong reference may not change that.
  • SoftReference: The collector decides when to clear it in response to memory demand. Sufficient free memory does not guarantee indefinite retention. Android recommends a capacity-controlled LruCache for most caches instead of relying on soft-reference timing.
  • WeakReference: The collector clears relevant weak references when it processes a weakly reachable object. This is not synchronous with losing a particular strong reference. An object that is still softly reachable is not yet weakly reachable.
  • PhantomReference: get() always returns null; a ReferenceQueue can support collection-related notification and cleanup. It does not replace explicit close() or guarantee timely work after finalize().

For a strong-reference leak, usually start by excluding weak/soft/phantom referent paths, then inspect the full graph as needed. with all references includes non-strong paths, so not every displayed path is evidence that GC is being prevented. See the Path to GC Roots arguments.

The historical screenshot below shows Path to GC Roots → with all references:
image

Reference Documents

  1. Eclipse MAT downloads and runtime requirements
  2. MAT: Shallow and Retained Heap
  3. MAT: Dominator Tree
  4. Android Studio: Capture a heap dump
  5. Android: Bitmap memory management

About Me && Blog

Below are personal introduction and related links. I hope to communicate more with everyone in the industry. If three people walk together, there must be one who can be my teacher!

  1. Blogger Personal Introduction: There are personal WeChat and WeChat group links inside.
  2. This Blog Content Navigation: A navigation of personal blog content.
  3. Excellent blog articles organized and collected by individuals - A must-know for Android efficiency optimization: Everyone is welcome to recommend themselves and recommend others (WeChat private chat is fine)
  4. Android Performance Optimization Knowledge Planet: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.

One person can go faster, a group of people can go further

WeChat Scan

CATALOG
  1. 1. Introduction to MAT
    1. 1.1. What is MAT?
    2. 1.2. Download and Installation of MAT Tool
  2. 2. Common Memory and Resource-Management Problems in Android (Java)
    1. 2.1. Android Memory
    2. 2.2. Common Cases of Improper Memory Usage
      1. 2.2.1. Cursor Not Closed After Querying Database
      2. 2.2.2. Not Using Cached convertView When Constructing Adapter
      3. 2.2.3. Bitmap Cleanup Depends on Version and Ownership
      4. 2.2.4. Releasing References to Objects
      5. 2.2.5. Others
  3. 3. Using MAT for Memory Debugging
    1. 3.1. Obtaining HPROF File
      1. 3.1.1. Current Entry Point: Android Studio Profiler
      2. 3.1.2. Historical Entry Point: Eclipse / DDMS
      3. 3.1.3. Export Format and Conversion
    2. 3.2. Introduction to MAT Main Interface
    3. 3.3. Introduction to Some Concepts in MAT
      1. 3.3.1. 3.3.1 Shallow heap
      2. 3.3.2. 3.3.2 Retained Heap
      3. 3.3.3. GC Root
    4. 3.4. Some Useful Views in MAT
      1. 3.4.1. Thread Overview
      2. 3.4.2. Group
      3. 3.4.3. Path to GC Root
  4. 4. Reference Documents
  5. 5. About Me && Blog