Android内存溢出OOM简单介绍

前言

在日常的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中内存介绍

在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配置项的大小。

  • dalvik.vm.heapstartsize相当于虚拟机的-Xms配置,该项用来设置堆内存的初始大小。
  • dalvik.vm.heapgrowthlimit相当于虚拟机的 -XX:HeapGrowthLimit配置,该项用来设置一个标准的应用的最大堆内存大小。一个标准的应用就是没有使用android:largeHeap的应用。
  • dalvik.vm.heapsize相当于虚拟机的-Xmx配置,该项设置了使用android:largeHeap的应用的最大堆内存大小。
  • dalvik.vm.heaptargetutilization相当于虚拟机的 -XX:HeapTargetUtilization,该项用来设置当前理想的堆内存利用率。其取值位于0与1之间。当GC进行完垃圾回收之后,Dalvik的堆内存会进行相应的调整,通常结果是当前存活的对象的大小与堆内存大小做除法,得到的值为这个选项的设置,即这里的0.75。注意,这只是一个参考值,Dalvik虚拟机也可以忽略此设置
  • dalvik.vm.heapminfree对应的是-XX:HeapMinFree配置,用来设置单次堆内存调整的最小值。
  • dalvik.vm.heapmaxfree对应的是-XX:HeapMaxFree配置,用来设置单次堆内存调整的最大值。通常情况下,还需要结合上面的 -XX:HeapTargetUtilization的值,才能确定内存调整时,需要调整的大小。

一般情况下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]

-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
  • VSS – Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)。
  • RSS – Resident Set Size 实际使用物理内存(包含共享库占用的内存)。
  • PSS – Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)。
  • USS – Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)。

USS是针对某个进程开始有可疑内存泄露的情况,是一个程序启动了会产生的虚拟内存,一旦这个程序进程杀掉就会释放!不过USS需要通过root的手机。一般没有root的手机我们可以获取PSS。一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

有关不同内存的详细信息,这里重点列出ViewRootImpl、AppContexts和Activities。

  • ViewRootImpl当前活动应用的根视图数量。每个根视图都与一个窗口关联,因此有助于您确定涉及对话框或其他窗口的内存泄漏。
  • AppContexts 和 Activities当前活动的应用 Context 和 Activity 对象数量。这可以帮助您快速确定由于存在静态引用(比较常见)而无法进行垃圾回收的已泄漏 Activity 对象。这些对象经常拥有很多关联的其他分配,因此成为跟踪大型内存泄漏的一种不错的方式。

有关内存整体分配的更多内容可以参看调查 RAM 使用情况

小结

本篇主要介绍有关内存溢出的一些概念,以及在Android中如何查看相关内存信息。后续再继续介绍有关内存溢出的几种常见现象,并介绍如何根据不同现象的内存泄漏做出进一步的解决方式。最后会介绍在开发中一旦发生了内存泄漏,该如何使用工具进行探测分析内存溢出。

评论

您确定要删除吗?删除之后不可恢复