Bitmap Memory: Native Heap, Hardware Bitmaps, and inBitmap Reuse
Bitmap Memory: Native Heap, Hardware Bitmaps, and inBitmap Reuse
A Bitmap looks like an ordinary Java object, but most of its weight lives somewhere the garbage collector cannot directly see. A single full screen photo can occupy tens of megabytes of pixel data, and where that data is allocated, how it is counted, and when it is freed has changed across Android versions. Understanding the storage model behind Bitmap explains why decoding a small file can throw OutOfMemoryError, why a hardware bitmap cannot be read pixel by pixel, and how image loading libraries keep memory under control. By the end of this lesson, you will be able to:
- Explain where a
Bitmap's pixel data is allocated and howNativeAllocationRegistrylinks that allocation to garbage collection. - Describe how
Bitmap.Configdetermines per pixel cost and compute a bitmap's byte count. - Explain what a hardware bitmap is, why it is immutable, and when pixel access throws.
- Trace how
BitmapFactory.Options.inBitmapreuses an existing allocation and the rules that govern it. - Apply downsampling and config choices to keep bitmap memory bounded.
Where the pixels live
A Bitmap instance is small. If you look at the fields it holds:
public final class Bitmap implements Parcelable {
private final long mNativePtr;
private int mWidth;
private int mHeight;
private WeakReference<HardwareBuffer> mHardwareBuffer;
private boolean mRecycled;
...
}
The mNativePtr is a pointer to a native object that owns the pixel buffer. On modern Android (8.0, API 26, and later) the pixel data is allocated in the native heap, not the Java heap. Before that, on Android 7.1 and earlier, pixels lived in the ART heap, which made bitmaps a frequent cause of fragmentation and OutOfMemoryError because they competed with regular objects for a relatively small heap. Moving pixels to the native heap let bitmaps use the much larger process address space and removed them from the Java GC's working set.
Native memory still needs to be reclaimed when the Bitmap object becomes unreachable. The framework wires this up with NativeAllocationRegistry. Looking at the Bitmap constructor:
mNativePtr = nativeBitmap;
final int allocationByteCount = getAllocationByteCount();
getRegistry(fromMalloc, allocationByteCount).registerNativeAllocation(this, mNativePtr);
registerNativeAllocation ties the lifetime of the native allocation to the lifetime of the Java Bitmap. When the Bitmap is collected, the registry runs the native free function on mNativePtr. Passing allocationByteCount matters: it tells the runtime how much native memory this Java object is keeping alive, so the GC accounts for that pressure when deciding whether to run a collection. A 48 MB bitmap is a 48 MB hint to the collector even though the Java object itself is tiny.
There are three places the pixel buffer can actually live, decided in native code in Bitmap.cpp:
- Heap:
allocateHeapBitmapdoes a plaincalloc(size, 1). This is the default. - Ashmem (shared memory):
allocateAshmemBitmapcallsashmem_create_regionthenmmapwithMAP_SHARED. This is used when a bitmap needs to cross a process boundary, for example when sent through aParcelor exposed withasShared(). - Hardware:
allocateHardwareBitmapallocates anAHardwareBufferand uploads the pixels to graphics memory. More on this below.
The getRegistry(boolean malloc, ...) call distinguishes the malloc backed case (heap) from the non malloc case (ashmem, which is mmap backed), because the two need different free functions.
You can ask for the native memory to be released eagerly with recycle():
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
mHardwareBuffer = null;
}
}
After recycle() the bitmap is marked dead: calling getPixels() or drawing it throws or does nothing. As the source comment notes, recycle() does not synchronously free the pixel data, it allows it to be collected once no other native references remain. On modern Android you rarely need recycle(), since the registry handles cleanup, but it is still useful when you want to drop a large allocation immediately rather than waiting for the next GC.
Counting the bytes: Config and byte count
How much memory a bitmap uses is width times height times bytes per pixel. The bytes per pixel comes from Bitmap.Config. The values, from the enum declaration:
public enum Config {
ALPHA_8(1),
RGB_565(3),
ARGB_4444(4),
ARGB_8888(5),
RGBA_F16(6),
HARDWARE(7),
RGBA_1010102(8);
}
The numbers in parentheses are native identifiers, not byte sizes. The per pixel costs are: ALPHA_8 is 1 byte (alpha only, used for masks), RGB_565 is 2 bytes (5 bits red, 6 green, 5 blue, no alpha), ARGB_8888 is 4 bytes (8 bits per channel, the default and the one most apps should use), RGBA_F16 is 8 bytes (half precision float per channel, for wide gamut and HDR content), and RGBA_1010102 is 4 bytes. ARGB_4444 is deprecated: since KitKat any request for it is silently serviced as ARGB_8888.
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor