Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Bitmap Limit - Preventing java.lang.OutOfMemory

I'm currently struggling with an odd behavior of the Android platform -- the Bitmap / Java heap memory limit. Depending on the device, Android limits the app developer to 16, 24, or 32 MiB of Java heap space (or you might find any random value on a rooted phone). This is arguably quite small, but relatively straightforward as I can measure usage with the following API's:

Runtime rt = Runtime.getRuntime();
long javaBytes = rt.totalMemory() - rt.freeMemory();
long javaLimit = rt.maxMemory();

Easy enough; now for the twist. In Android, bitmaps, with few exceptions, are stored in the native heap and do not count towards the Java heap. Some bright-eyed, purist developer at Google decided that this was "bad" and allowed the developer to get "more than their fair share." So there is this nice little piece of code that calculates the native memory usage incurred by bitmaps, and possibly other resources, and sums that with the Java heap and if you go over ..... java.lang.OutOfMemory. ouch

But no big deal. I have a lot of bitmaps, and don't need all of them all the time. I can "page out" some of the ones that aren't being used at the moment:

So, for attempt #1, I refactored the code so I could wrap every single bitmap load with a try/catch:

while(true) {
    try {
        return BitmapFactory.decodeResource(context.getResources(), android_id, bitmapFactoryOptions);
    } catch (java.lang.OutOfMemory e) {
        // Do some logging

        // Now free some space (the code below is a simplified version of the real thing)
        Bitmap victim = selectVictim();
        victim.recycle();
        System.gc(); // REQUIRED; else, weird behavior ensues
    }
}

See, here's a nice little log snippet showing my code catching the exception, and recycling some bitmaps:

E/Epic    (23221): OUT_OF_MEMORY (caught java.lang.OutOfMemory)
I/Epic    (23221): ArchPlatform[android].logStats() -
I/Epic    (23221): LoadedClassCount=0.00M
I/Epic    (23221): GlobalAllocSize=0.00M
I/Epic    (23221): GlobalFreedSize=0.02M
I/Epic    (23221): GlobalExternalAllocSize=0.00M
I/Epic    (23221): GlobalExternalFreedSize=0.00M
I/Epic    (23221): EpicPixels=26.6M (this is 4 * #pixels in all loaded bitmaps)
I/Epic    (23221): NativeHeapSize=29.4M
I/Epic    (23221): NativeHeapAllocSize=25.2M
I/Epic    (23221): ThreadAllocSize=0.00M
I/Epic    (23221): totalMemory()=9.1M
I/Epic    (23221): maxMemory()=32.0M
I/Epic    (23221): freeMemory()=4.4M
W/Epic    (23221): Recycling bitmap 'game_word_puzzle_11_aniframe_005'
I/Epic    (23221): BITMAP_RECYCLING: recycled 1 bitmaps worth 1.1M).  age=294

Note how totalMemory - freeMemory is only 4.7 MiB, but with ~26? MiB of native memory taken up by bitmaps, we are in the 31/32 MiB range where we hit the limit. I'm still a bit confused here as my running tally of all loaded bitmaps is 26.6 MiB, yet the native alloc size is only 25.2 MiB. So I'm counting something wrong. But it's all in the ballpark and definately demonstrates the cross-pool "summation" happening with the mem-limit.

I THOUGHT I had it fixed. But no, Android would not give up so easily...

Here's what I get from two of my four test devices:

I/dalvikvm-heap(17641): Clamp target GC heap from 32.687MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 24ms
D/dalvikvm(17641): GC_EXTERNAL_ALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 29ms
E/dalvikvm-heap(17641): 1111200-byte external allocation too large for this process.
E/dalvikvm(17641): Out of memory: Heap Size=7815KB, Allocated=4684KB, Bitmap Size=24443KB, Limit=32768KB
E/dalvikvm(17641): Trim info: Footprint=7815KB, Allowed Footprint=7815KB, Trimmed=880KB
E/GraphicsJNI(17641): VM won't let us allocate 1111200 bytes
I/dalvikvm-heap(17641): Clamp target GC heap from 32.686MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 17ms
I/DEBUG   ( 1505): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   ( 1505): Build fingerprint: 'verizon_wwe/htc_mecha/mecha:2.3.4/GRJ22/98797:user/release-keys'
I/DEBUG   ( 1505): pid: 17641, tid: 17641
I/DEBUG   ( 1505): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
I/DEBUG   ( 1505):  r0 0055dab8  r1 00000000  r2 00000000  r3 0055dadc
I/DEBUG   ( 1505):  r4 0055dab8  r5 00000000  r6 00000000  r7 00000000
I/DEBUG   ( 1505):  r8 000002b7  r9 00000000  10 00000000  fp 00000384
I/DEBUG   ( 1505):  ip 0055dab8  sp befdb0c0  lr 00000000  pc ab14f11c  cpsr 60000010
I/DEBUG   ( 1505):  d0  414000003f800000  d1  2073646565637834
I/DEBUG   ( 1505):  d2  4de4b8bc426fb934  d3  42c80000007a1f34
I/DEBUG   ( 1505):  d4  00000008004930e0  d5  0000000000000000
I/DEBUG   ( 1505):  d6  0000000000000000  d7  4080000080000000
I/DEBUG   ( 1505):  d8  0000025843e7c000  d9  c0c0000040c00000
I/DEBUG   ( 1505):  d10 40c0000040c00000  d11 0000000000000000
I/DEBUG   ( 1505):  d12 0000000000000000  d13 0000000000000000
I/DEBUG   ( 1505):  d14 0000000000000000  d15 0000000000000000
I/DEBUG   ( 1505):  d16 afd4242840704ab8  d17 0000000000000000
I/DEBUG   ( 1505):  d18 0000000000000000  d19 0000000000000000
I/DEBUG   ( 1505):  d20 0000000000000000  d21 0000000000000000
I/DEBUG   ( 1505):  d22 0000000000000000  d23 0000000000000000
I/DEBUG   ( 1505):  d24 0000000000000000  d25 0000000000000000
I/DEBUG   ( 1505):  d26 0000000000000000  d27 0000000000000000
I/DEBUG   ( 1505):  d28 00ff00ff00ff00ff  d29 00ff00ff00ff00ff
I/DEBUG   ( 1505):  d30 0000000000000000  d31 3fe55167807de022
I/DEBUG   ( 1505):  scr 68000012

That's a native crash. A segfault no less (sig11). By definition, a segfault is ALWAYS a bug. This is absolutely an Android bug in the native code handling GC and/or mem-limit checking. But it's still my app that crashes resulting in bad reviews, returns, and lower sales.

So I have to compute the limit myself. Except that I've struggled here. I tried adding up the pixels myself (EpicPixels), but I still hit the memcrash periodically, so I'm undercounting something. I tried adding the javaBytes (total - free) to NativeHeapAllocSize, but this would occassionally cause my app to become "anorexic", freeing and freeing bitmaps until there was nothing left to purge.

  1. Does anyone know the exact computation used to calculate the memory limit and trigger java.lang.OutOfMemory?

  2. Has anyone else hit this issue and worked through it? Do you have any pearls of wisdom?

  3. Does anyone know which Google employee dreamed up this scheme so I can punch him for ruining ~40 hrs of my life? j/k

ANSWER: The limit is for NativeHeapAllocSize < maxMemory(); however, due to memory fragmentation, Android crashes well before the actual limit. Thus, you have to limit yourself to a value somewhat less than the actual limit. This "safety factor" is app dependent, but a few MiB seems to work for most people. (can I just say that I'm blown away by how broken this behavior is)

like image 887
Dave Dopson Avatar asked Jul 31 '11 22:07

Dave Dopson


2 Answers

The limit varies over each device (Use the 3rd link if you want to load the bitmap as is) or here you have some tricks to avoid that issue like:

  • use the onLowMemory() of the Application class for freeing some memory avoiding the crash.
  • Indicate the desired size for the bitmap before decoding it. Check that links for more info:

http://davidjhinson.wordpress.com/2010/05/19/scarce-commodities-google-android-memory-and-bitmaps/

Strange out of memory issue while loading an image to a Bitmap object

This link show to check the heap

BitmapFactory OOM driving me nuts

  • And of course free the memory of old bitmaps
like image 200
Addev Avatar answered Nov 16 '22 07:11

Addev


OK, so I'm starting to suspect that the limit in native mode is enforced on total java heap size + native used memory.

The limit is based on NativeHeapAllocSize vs. maxMemory(). You will see below that I'm crashing allocating ~1 MiB while I'm at 22.0 MiB / 24 MiB. The limit is an UPPER BOUND on how much memory you can allocate. This is what threw me for a while. The crash happens significantly before you hit the limit. Thus, the need for a "memoryPad" value in the solution, as trying to alloc 23.999 MiB / 24 MiB will result in a crash nearly 100% of the time. So if the limit is 24 MiB, how much can you safely use?? Unknown. 20 MiB seems to work. 22 MiB seems to work. I'm nervous pushing any closer than that. The ammount varies depending on how fragmented the malloc memory space is in the native process. And of course, there is no way to measure any of this, so err on the safe side.

07-31 18:37:19.031: WARN/Epic(3118): MEMORY-USED: 27.3M = 4.2M + 23.0M.  jf=1.7M, nhs=23.3M, nhf=0.0M
07-31 18:37:19.081: INFO/Epic(3118): ArchPlatform[android].logStats() - 
07-31 18:37:19.081: INFO/Epic(3118): LoadedClassCount=0.00M
07-31 18:37:19.081: INFO/Epic(3118): GlobalAllocSize=0.02M
07-31 18:37:19.081: INFO/Epic(3118): GlobalFreedSize=0.05M
07-31 18:37:19.081: INFO/Epic(3118): GlobalExternalAllocSize=0.00M
07-31 18:37:19.081: INFO/Epic(3118): GlobalExternalFreedSize=0.00M
07-31 18:37:19.081: INFO/Epic(3118): EpicPixels=17.9M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapSize=22.2M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapFree=0.07M
07-31 18:37:19.081: INFO/Epic(3118): NativeHeapAllocSize=22.0M
07-31 18:37:19.081: INFO/Epic(3118): ThreadAllocSize=0.12M
07-31 18:37:19.081: INFO/Epic(3118): totalMemory()=5.7M
07-31 18:37:19.081: INFO/Epic(3118): maxMemory()=24.0M
07-31 18:37:19.081: INFO/Epic(3118): freeMemory()=1.6M
07-31 18:37:19.081: INFO/Epic(3118): app.mi.availMem=126.5M
07-31 18:37:19.081: INFO/Epic(3118): app.mi.threshold=16.0M
07-31 18:37:19.081: INFO/Epic(3118): app.mi.lowMemory=false
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikPrivateDirty=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikPss=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.dalvikSharedDirty=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativePrivateDirty=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativePss=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.nativeSharedDirty=0.00M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherPrivateDirty=0.02M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherPss0.02M
07-31 18:37:19.081: INFO/Epic(3118): dbg.mi.otherSharedDirty=0.00M
07-31 18:37:19.081: ERROR/dalvikvm-heap(3118): 1111200-byte external allocation too large for this process.
07-31 18:37:19.081: ERROR/dalvikvm(3118): Out of memory: Heap Size=6535KB, Allocated=4247KB, Bitmap Size=17767KB
07-31 18:37:19.081: ERROR/GraphicsJNI(3118): VM won't let us allocate 1111200 bytes

The code to print all that out:


    public static void logMemoryStats() {
        String text = "";
        text += "\nLoadedClassCount="               + toMib(android.os.Debug.getLoadedClassCount());
        text += "\nGlobalAllocSize="                + toMib(android.os.Debug.getGlobalAllocSize());
        text += "\nGlobalFreedSize="                + toMib(android.os.Debug.getGlobalFreedSize());
        text += "\nGlobalExternalAllocSize="        + toMib(android.os.Debug.getGlobalExternalAllocSize());
        text += "\nGlobalExternalFreedSize="        + toMib(android.os.Debug.getGlobalExternalFreedSize());
        text += "\nEpicPixels="                     + toMib(EpicBitmap.getGlobalPixelCount()*4);
        text += "\nNativeHeapSize="                 + toMib(android.os.Debug.getNativeHeapSize());
        text += "\nNativeHeapFree="                 + toMib(android.os.Debug.getNativeHeapFreeSize());
        text += "\nNativeHeapAllocSize="            + toMib(android.os.Debug.getNativeHeapAllocatedSize());
        text += "\nThreadAllocSize="                + toMib(android.os.Debug.getThreadAllocSize());

        text += "\ntotalMemory()="                  + toMib(Runtime.getRuntime().totalMemory());
        text += "\nmaxMemory()="                    + toMib(Runtime.getRuntime().maxMemory());
        text += "\nfreeMemory()="                   + toMib(Runtime.getRuntime().freeMemory());

        android.app.ActivityManager.MemoryInfo mi1 = new android.app.ActivityManager.MemoryInfo();
        ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
        am.getMemoryInfo(mi1);
        text += "\napp.mi.availMem="                + toMib(mi1.availMem);
        text += "\napp.mi.threshold="               + toMib(mi1.threshold);
        text += "\napp.mi.lowMemory="               + mi1.lowMemory;

        android.os.Debug.MemoryInfo mi2 = new android.os.Debug.MemoryInfo();        
        Debug.getMemoryInfo(mi2);
        text += "\ndbg.mi.dalvikPrivateDirty="      + toMib(mi2.dalvikPrivateDirty);
        text += "\ndbg.mi.dalvikPss="               + toMib(mi2.dalvikPss);
        text += "\ndbg.mi.dalvikSharedDirty="       + toMib(mi2.dalvikSharedDirty);
        text += "\ndbg.mi.nativePrivateDirty="      + toMib(mi2.nativePrivateDirty);
        text += "\ndbg.mi.nativePss="               + toMib(mi2.nativePss);
        text += "\ndbg.mi.nativeSharedDirty="       + toMib(mi2.nativeSharedDirty);
        text += "\ndbg.mi.otherPrivateDirty="       + toMib(mi2.otherPrivateDirty);
        text += "\ndbg.mi.otherPss"                 + toMib(mi2.otherPss);
        text += "\ndbg.mi.otherSharedDirty="        + toMib(mi2.otherSharedDirty);

        EpicLog.i("ArchPlatform[android].logStats() - " + text);
    }
like image 33
Dave Dopson Avatar answered Nov 16 '22 07:11

Dave Dopson