Fixing java.lang.OutOfMemoryError When Loading Large Bitmaps in Android

intermediate๐Ÿ“ฑ Android2026-05-21| Android (API 21+), Java/Kotlin, Android Studio, devices with limited heap memory

Error Message

java.lang.OutOfMemoryError: Failed to allocate a 52428800 byte allocation with 8388608 free bytes and 7MB until OOM
#bitmap#memory#glide#picasso#oom

The Error

You're loading an image โ€” maybe from a gallery picker, a camera capture, or a remote URL โ€” and the app crashes with:

java.lang.OutOfMemoryError: Failed to allocate a 52428800 byte allocation with 8388608 free bytes and 7MB until OOM

That 52428800 bytes is 50 MB. Android tried to allocate 50 MB for a single bitmap, found only ~8 MB free in the heap, and bailed. The app is dead.

Why This Happens

Android decodes images into raw pixel data in memory. A 12 MP photo (4032ร—3024) decoded as ARGB_8888 uses:

4032 ร— 3024 ร— 4 bytes = ~46.5 MB

That's one photo. The default heap on many mid-range devices is 48โ€“128 MB total, shared with everything else the app is doing. Load two or three uncompressed bitmaps and you've blown the budget.

Common triggers:

  • Loading full-resolution camera photos directly into an ImageView
  • Decoding images inside a RecyclerView without recycling bitmaps
  • Using BitmapFactory.decodeFile() without sampling options
  • Keeping references to bitmaps in static fields or singletons
  • Loading images with Glide/Picasso but bypassing their caching (e.g., calling .skipMemoryCache(true) everywhere)

Step 1 โ€” Confirm the Source in Logcat

Find the exact call that's crashing before touching any code. Run the app, reproduce the crash, then filter Logcat:

adb logcat | grep -E "OutOfMemoryError|BitmapFactory|alloc"

Or in Android Studio, filter by OutOfMemoryError. The stack trace will point to the exact line โ€” usually a BitmapFactory.decode* call or an image library loader.

Step 2 โ€” If You're Using Raw BitmapFactory

Never decode a full image without sampling it down to fit the target view. Use BitmapFactory.Options:

fun decodeSampledBitmap(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap {
    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true  // decode size only, no pixels
    }
    BitmapFactory.decodeFile(filePath, options)

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
    options.inJustDecodeBounds = false

    return BitmapFactory.decodeFile(filePath, options)
}

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (height, width) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    if (height > reqHeight || width > reqWidth) {
        val halfHeight = height / 2
        val halfWidth = width / 2
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    return inSampleSize
}

Pass the actual display size of your ImageView โ€” not a hardcoded guess:

imageView.post {
    val bmp = decodeSampledBitmap(filePath, imageView.width, imageView.height)
    imageView.setImageBitmap(bmp)
}

This can shrink a 46 MB allocation down to 3โ€“4 MB with zero visible quality loss in a thumbnail.

Step 3 โ€” Switch to RGB_565 for Non-Transparent Images

Photos and background images rarely need alpha transparency. Use RGB_565 instead of the default ARGB_8888 โ€” it stores 2 bytes per pixel instead of 4, cutting memory in half:

val options = BitmapFactory.Options().apply {
    inPreferredConfig = Bitmap.Config.RGB_565
    inSampleSize = 4  // or your calculated value
}
val bitmap = BitmapFactory.decodeFile(filePath, options)

Step 4 โ€” If You're Using Glide

Glide handles most of this automatically. OOM still sneaks in when you override the target size to full resolution, or load into a view that's wrap_content with no explicit dimensions.

Explicitly cap the dimensions and use the lighter pixel format:

Glide.with(context)
    .load(imageUrl)
    .override(800, 600)          // cap dimensions
    .format(DecodeFormat.PREFER_RGB_565)  // half the memory
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView)

For full-screen banners where you want screen-width resolution without a fixed pixel size:

Glide.with(context)
    .load(imageUrl)
    // .override(Target.SIZE_ORIGINAL)  // avoid this unless you truly need full res
    .override(Resources.getSystem().displayMetrics.widthPixels, 0)  // screen width, auto height
    .into(imageView)

Also check that you're not holding a Target reference in a static field โ€” that's a reliable way to prevent Glide from ever releasing the bitmap.

Step 5 โ€” If You're Using Picasso

Picasso's API is more explicit about sizing. Set resize() and onlyScaleDown() together so you cap large images without accidentally upscaling small ones:

Picasso.get()
    .load(imageUrl)
    .resize(800, 600)
    .onlyScaleDown()
    .centerCrop()
    .into(imageView)

For local files, pass the URI rather than the raw file path. Picasso handles file:// URIs cleanly and won't double-buffer the data the way a raw path sometimes does.

Step 6 โ€” Recycle Bitmaps You No Longer Need

Managing bitmaps manually โ€” without Glide or Picasso โ€” means you own the cleanup. Call recycle() when the view detaches:

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    val drawable = imageView.drawable
    if (drawable is BitmapDrawable) {
        drawable.bitmap?.recycle()
    }
    imageView.setImageDrawable(null)
}

One hard rule: never call recycle() on a bitmap that's still being displayed. It causes Canvas: trying to use a recycled bitmap crashes that are worse to debug than the OOM you started with.

Step 7 โ€” Check for Memory Leaks with the Profiler

OOM from leaks looks different from a single large allocation. Heap usage climbs steadily on each navigation and never drops back. Use Android Studio's Memory Profiler to spot this:

  • Run the app in debug mode โ†’ View โ†’ Tool Windows โ†’ Profiler
  • Click the Memory track
  • Navigate to the screen that loads images, go back, repeat 5 times
  • Click Force GC (the garbage can icon)
  • If heap usage doesn't drop back toward baseline, you have a leak

Click Capture heap dump and look for Bitmap instances that should have been released. Even one stuck bitmap can be 46 MB of permanent overhead.

Step 8 โ€” Increase largeHeap Only as a Last Resort

Adding android:largeHeap="true" to your AndroidManifest.xml roughly doubles the available heap on most devices:

<application
    android:largeHeap="true"
    ...>

It buys time, nothing more. The crash will return as soon as the app loads slightly more data, and some OEM devices ignore the flag entirely. Use it only while you're working on the real fix โ€” or for apps with genuinely heavy memory needs like photo editors.

Verify the Fix

  • Reproduce the exact flow that crashed before โ€” no crash means the fix is holding
  • Watch the Memory Profiler during image loading and confirm allocations stay flat
  • Check Logcat for any remaining Bitmap allocation warnings
  • Test on a low-end device (1โ€“2 GB RAM), or set an emulator to 512 MB RAM โ€” problems that hide on a flagship show up immediately here

Lessons Learned

  • Never call BitmapFactory.decode* without inJustDecodeBounds + inSampleSize
  • Glide and Picasso handle the hard parts โ€” let them manage the lifecycle rather than reimplementing it manually
  • A 12 MP photo is always ~46 MB in memory, no matter how small the JPEG is on disk
  • RGB_565 is free memory savings for any image without transparency
  • Leaks are harder to spot than single large allocations โ€” profile first before assuming it's a one-time load problem

Related Error Notes