在日常的Android开发中,每个开发者或多或少都会遇到过OutOfMemoryError这样崩溃信息。如果工程稍微大一些,在monkey测试的崩溃日志也是比较常见的一种。如下是比较常见的一些报错信息:
Android:java.lang.OutOfMemoryError: Failed to allocate a 1340012 byte allocation with 72503 free bytes and 70KB until OOM OutOfMemoryError: (Heap Size=49187KB, Allocated=41957KB) COMPILETODALVIK : UNEXPECTED TOP-LEVEL error : java.lang.OutOfMemoryError: Java heap space ... java.lang.StackOverflowError ...
相对于StackOverflowError而言OutOfMemoryError则是比较常见的一种内存异常。在《深入理解Java虚拟机》一书中有对这两种异常信息的简单描述。
如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可以动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError。
StackOverflowError不常见,主要在渲染复杂布局或者动态缓存数据到数据库时比较常见。而OutOfMemoryError只要内存泄漏多了就有可能导致内存溢出。
内存泄漏 Memeory Leak程序在向系统申请分配内存空间后(new),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。
内存溢出 Out Of Memory是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如内存只能分配一个int类型,我却要塞给他一个long类型,系统就出现oom。又比如一车最多能坐5个人,你却非要塞下10个,车就挤爆了。
内存泄漏的堆积最终会导致内存溢出。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。
为了判断Java中是否有内存泄漏,首先我们必须了解Java是如何管理内存的。Java的内存管理可以简单理解为对象的分配和释放。在Java中内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)自动完成的。程序员不需要自己调用方法释放内存,但它只能回收无用且不被其它对象引用的那些对象占用的空间。
Java内存回收机制就是从程序的GC root(静态对象以及堆内存对象等)开始检查引用链,当遍历一遍之后得到上述无法回收的对象以及它们所引用的对象链,组成无法回收的对象集合,而其它孤立的对象就作为垃圾回收。GC为了能够正确释放对象,必须监控每一个对象的状态,包括对象的申请、引用、被引用、赋值等,GC都需要对其进行监控。监控对象的状态是为了更加准确地、及时地释放对象,而释放对象的基本原则就是该对象不再被使用。
在Java中上述的那些无用的对象由GC负责回收,因此程序员不需要考虑这部分内存泄漏。虽然我们有几个方法可以方位GC,例如System.gc(),但是根据Java语言规范的定义,该方法并不会保证Java的垃圾收集器一定会执行。因为不同的JVM实现者可能使用的是不同的算法实现的GC。通常GC线程的优先级比较低。JVM调用GC的策略也有许多中,有的是当内存使用的一定限度时才会执行GC,也有定时执行的。再者Java编程规范也不建议开发人员自己手动调用System.gc()。
Android应用程序采用Java编程语言编写,根据上面描述,Java区别于其他语言的一个重要优点就是它通过GC 自动管理内存的回收,Android程序员只需通过内存分配操作创建对象,而无须关心对象占用的空间是如何被收回的。因此很多程序员认为在Java中不必担心内存泄漏的问题,然而实际并非如此,Java中仍然存在着内存泄漏。Android应用程序一般都是运行在移动设备中的,而移动设备中内存的总量非常有限,因此如何合理地规避“内存泄露”问题也就显得十分关键。
在Android中获取系统分配内存的大小一般采用如下两种方式:
ActivityManager mgr= (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); int memSize=mgr.getMemoryClass(); int maxSize=mgr.getLargeMemoryClass(); builder.append("系统分配内存大小:"+memSize+"M/n"); builder.append("系统分配最大内存:"+maxSize+"M/n/n"); long totalMem=Runtime.getRuntime().totalMemory()/SIZE_UNIT; long freeMem=Runtime.getRuntime().freeMemory()/SIZE_UNIT; long maxMem=Runtime.getRuntime().maxMemory()/SIZE_UNIT; builder.append("总内存:"+totalMem+"M/n"); builder.append("剩余内存:"+freeMem+"M/n"); builder.append("最大内存:"+maxMem+"M/n");
点击进去可查看到getMemoryClass()和getLargeMemoryClass()源码,实际上它们读取的是配置文件中的数值,文件目录位于 /system/build.prop
。
static public int staticGetMemoryClass() { // Really brain dead right now -- just take this from the configured // vm heap size, and assume it is in megabytes and thus ends with "m". String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", ""); if (vmHeapSize != null && !"".equals(vmHeapSize)) { return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1)); } return staticGetLargeMemoryClass(); }
通过adb命令 cat /system/build.prop | grep heap
,我们可以查看dalvik内存的配置信息,如下是两个手机中的配置信息:
dalvik.vm.heapstartsize=8m dalvik.vm.heapgrowthlimit=256m dalvik.vm.heapsize=512m dalvik.vm.heaptargetutilization=0.75 dalvik.vm.heapminfree=512k dalvik.vm.heapmaxfree=8m dalvik.vm.heapstartsize=8m dalvik.vm.heapgrowthlimit=96m dalvik.vm.heapsize=256m dalvik.vm.heaptargetutilization=0.75 dalvik.vm.heapminfree=512k dalvik.vm.heapmaxfree=8m
getMemoryClass()获取到的是dalvik.vm.heapgrowthlimit的大小,而getLargeMemoryClass()获取到的就是dalvik.vm.heapsize配置项的大小。
一般情况下Runtime.getRuntime().maxMemory()获取到的最大内存大小跟getMemoryClass()大小相同,但是如果在清单文件AndroidManifest.xml中设置了 android:largeHeap="true"
,这时候获取到的值可能就是getLargeMemoryClass()大小,这里也是一般而言,因为有部分手机中设置了 android:largeHeap="true"
也无效。还有一点需要注意,使用ActivityManager的getMemoryClass()或者getLargeMemoryClass()获取到的内存值的单位是M,但是Runtime.getRuntime()获取到的是bit。
我们可以使用下面的 adb 命令观察应用内存在不同类型的 RAM 分配之间的划分情况:
adb shell dumpsys meminfo
-d 标志会打印与 Dalvik 和 ART 内存使用情况相关的更多信息。输出列出了应用的所有当前分配,单位为千字节。
如下是一个简单的demo的dumpsys:
Applications Memory Usage (in Kilobytes): Uptime: 544929 Realtime: 544929 ** MEMINFO in pid 1712 [com.sunny.memory] ** Pss Private Private SwapPss Heap Heap Heap Total Dirty Clean Dirty Size Alloc Free ------ ------ ------ ------ ------ ------ ------ Native Heap 3490 3280 0 0 14336 12849 1486 Dalvik Heap 826 800 0 0 2213 677 1536 Dalvik Other 696 696 0 0 Stack 72 72 0 0 Ashmem 6 0 0 0 Other dev 10 0 8 0 .so mmap 1625 160 20 0 .apk mmap 3032 2316 12 0 .ttf mmap 96 0 0 0 .dex mmap 1794 4 144 0 .oat mmap 457 0 0 0 .art mmap 3876 3680 0 0 Other mmap 2334 4 1736 0 Unknown 458 436 0 0 TOTAL 18772 11448 1920 0 16549 13526 3022 App Summary Pss(KB) ------ Java Heap: 4480 Native Heap: 3280 Code: 2656 Stack: 72 Graphics: 0 Private Other: 2880 System: 5404 TOTAL: 18772 TOTAL SWAP PSS: 0 Objects Views: 17 ViewRootImpl: 1 AppContexts: 3 Activities: 1 Assets: 13 AssetManagers: 3 Local Binders: 10 Proxy Binders: 17 Parcel memory: 2 Parcel count: 10 Death Recipients: 0 OpenSSL Sockets: 0 WebViews: 0 SQL MEMORY_USED: 0 PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
USS是针对某个进程开始有可疑内存泄露的情况,是一个程序启动了会产生的虚拟内存,一旦这个程序进程杀掉就会释放!不过USS需要通过root的手机。一般没有root的手机我们可以获取PSS。一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
有关不同内存的详细信息,这里重点列出ViewRootImpl、AppContexts和Activities。
有关内存整体分配的更多内容可以参看 调查 RAM 使用情况 。
本篇主要介绍有关内存溢出的一些概念,以及在Android中如何查看相关内存信息。后续再继续介绍有关内存溢出的几种常见现象,并介绍如何根据不同现象的内存泄漏做出进一步的解决方式。最后会介绍在开发中一旦发生了内存泄漏,该如何使用工具进行探测分析内存溢出。