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.
Does anyone know the exact computation used to calculate the memory limit and trigger java.lang.OutOfMemory?
Has anyone else hit this issue and worked through it? Do you have any pearls of wisdom?
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)
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:
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
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With