OOM问题
Android中的OutOfMemoryError成因主要有以下几种:
- java堆内存溢出
为对象分配内存时达到进程的内存上限抛出。通过Runtime.getRuntime.MaxMemory()
可以得到Android中每个进程被系统分配的内存上限。 - 无足够连续内存空间
除了可用内存不够的原因外,没有足够的连续地址空间也会造成OOM,比如内存抖动造成频繁GC产生大量内存碎片。 - FD数量超出限制
创建线程第一步创建匿名共享内存时,需要打开/dev/ashmem文件,所以需要一个FD(文件描述符)。此时,如果创建的FD数已经达到上限,则会导致创建JNIEnv失败。 - 线程数量超出限制
- 虚拟内存不足
相关Java API介绍:
Runtime.getRuntime.MaxMemory();
Runtime.getRuntime.totalMemory();
Runtime.getRuntime.freeMemory()
// 得到当前进程的Java内存快照文件(即HPROF文件),比较耗时
Debug.dumpHprofData(String fileName);
// 进程中的所有线程以及对应的堆栈信息
Thread.getAllStackTraces();
相关Android API介绍:
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
//heapSize是设备分配给app的最大堆内存
int heapSize = manager.getMemoryClass();
// maxHeapSize 是当配置了android:largeHeap="true" 才有的最大堆内存,一般是heapSize的2-3倍
int maxHeapSize = manager.getLargeMemoryClass(); // manafest.xml android:largeHeap="true"
内存抖动
短时间内有大量对象创建与销毁,它伴随着频繁的GC,造成卡顿(stop the world阻塞)和内存碎片。比如字符串用“+”拼接,onDraw等方法中创建对象等。除了避免上述操作,灵活使用对象池也是一种改良手段。
内存泄漏
长生命周期对象持有短生命周期对象强引用,导致短生命周期对象无法被回收,软弱虚引用则不会泄漏。
常见的内存泄漏场景有:
- 静态成员变量/单例/订阅未注销
- Closable等资源没释放
- 异步任务回调
线程,Timer、handler
内存溢出
著名的OOM,当内存不足与创建大对象或创建新线程时会抛出。
最常见的OOM情况有以下三种:
- java.lang.OutOfMemoryError: Java heap space
java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。 - java.lang.OutOfMemoryError: PermGen space
java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。 - java.lang.StackOverflowError
不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
除了及时释放内存外,还可以在创建这些对象时对OOM进行捕获:
Bitmap bitmap = null;
try {
// 实例化Bitmap
bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
//OutOfMemoryError是一种Error,而不是Exception。在此仅仅做一下提醒,避免写错代码而捕获不到OutOfMemoryError
}
if (bitmap == null) {
// 如果实例化失败 返回默认的Bitmap对象
return defaultBitmapMap;
}
排查方法
hprof分析工具:haha
参考资料
JVM内存结构
JVM结构
按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:
- 程序计数器:当前线程执行的字节码的行号指示器,线程私有
- JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
- 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
- JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
- 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。
- 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
- 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。
除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。
JMM
Java内存模型(Java Memory Model,JMM)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
GC机制
几个重要的概念:
- Dominator:从GC Roots到达某一个对象时,必须经过的对象,称为该对象的Dominator。例如在上图中,B就是E的Dominator,而B却不是F的Dominator。
- ShallowSize:对象自身占用的内存大小,不包括它引用的对象。
- RetainSize:对象自身的ShallowSize和对象所支配的(可直接或间接引用到的)对象的ShallowSize总和,就是该对象GC之后能回收的内存总和。例如上图中,D的RetainSize就是D、H、I三者的ShallowSize之和。
JVM在进行GC的时候会进行可达性分析,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是可回收的。
GC日志
发生垃圾回收事件时,相应消息会输出到Logcat中。在此类日志消息积聚时,注意观察堆统计数据的增大情况,如果此值继续增大,可能会出现内存泄露。如果看到原因为“Alloc”的 GC,则意味着已快要达到堆容量上限,并且很快会出现OOM异常。
Dalvik日志
对于dalvik,其GC日志格式如下:
D/dalvikvm(PID): GC_Reason Amount_freed, Heap_stats, External_memory_stats, Pause_time
例如:
D/dalvikvm(9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
各个参数的含义如下:
参数名 | 含义 |
---|---|
GC_Reason | GC原因,如下表所示。 |
Amount_freed | 释放量,从此次GC中回收的内存量 |
Heap_stats | 堆统计数据,堆的可用空间百分比与(活动对象数量)/(堆总大小) |
External_memory_stats | 外部内存统计数据,API 级别 10 及更低级别的外部分配内存(已分配内存量)/(发生回收的限值) |
Pause_time | 暂停时间,堆越大,暂停时间越长。并发暂停时间显示两个暂停:一个出现在回收开始时,另一个出现在回收快要完成时。 |
GC原因表:
原因 | 说明 |
---|---|
GC_CONCURRENT | 在堆开始占用内存时释放内存的并发GC |
GC_FOR_MALLOC | 堆已满而系统不得不停止应用并回收内存时,应用尝试分配内存而引起的GC |
GC_HPROF_DUMP_HEAP | 请求创建 HPROF 文件来分析堆时发生的 GC |
GC_EXPLICIT | 显式GC,例如当调用 System.gc() 时(应避免调用它,而应信任GC会根据需要运行) |
GC_EXTERNAL_ALLOC | 仅适用于 API 级别 10 及更低级别(更新的版本会在 Dalvik 堆中分配任何内存)。外部分配内存的 GC(例如存储在原生内存或 NIO 字节缓冲区中的像素数据) |
ART日志
与Dalvik有所不同:
- 对于隐式GC:ART不会为未明确请求的GC记录消息。只有在系统认为GC速度较慢时才会输出GC消息。更确切地说,仅在GC暂停时间超过5毫秒或GC持续时间超过100毫秒时。如果应用未处于可察觉到暂停的状态(例如应用在后台运行时,这种情况下,用户无法察觉GC暂停),则其所有GC都不会被视为速度较慢。
- 对于现实GC:系统一直会记录显式GC。
ART的GC日志格式如下:
I/art: GC_Reason GC_Name Objects_freed(Size_freed) AllocSpace Objects,
Large_objects_freed(Large_object_size_freed) Heap_stats LOS objects, Pause_time(s)
例如:
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects,
21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
各个参数的含义如下:
参数名 | 含义 |
---|---|
GC_Reason | GC原因,如下表所示。 |
GC_Name | GC名称,如下表所示。 |
Objects_freed(Size_freed) | 释放的对象(释放的字节数大小),此GC从非大型对象空间回收的对象数量。 |
Large_objects_freed(Large_object_size_freed) | 释放的大型对象(释放的大型对象大小),此垃圾回收从大型对象空间回收的对象数量。 |
Heap_stats | 堆统计数据,可用空间百分比与(活动对象数量)/(堆总大小)。 |
Pause_time | 暂停时间,通常情况下,暂停时间与GC运行时修改的对象引用数量成正比。当前,ART CMSGC仅在GC即将完成时暂停一次。 移动GC的暂停时间较长,会在GC的大部分时间持续。 |
GC原因表:
原因 | 说明 |
---|---|
Concurrent | 不会挂起应用线程的并发 GC。此GC在后台线程中运行,而且不会阻止分配。 |
Alloc | 应用在堆已满时尝试分配内存而引起的 GC。在这种情况下,垃圾回收在分配线程中发生。 |
Explicit | 由应用明确请求的垃圾回收,例如,通过调用System.gc()或Runtime.getRuntime().gc()。与 Dalvik 一样,在 ART 中,最佳做法是您信任GC并避免请求显式 GC(如果可能)。不建议请求显式 GC,因为它们会阻止分配线程并不必要地浪费 CPU 周期。此外,如果显式GC导致其他线程被抢占,则也可能会导致卡顿(应用出现卡顿、抖动或暂停)。 |
NativeAlloc | 原生分配(例如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。 |
CollectorTransition | 由堆转换引起的回收;这由在运行时变更GC策略引起(例如应用在可察觉到暂停的状态之间切换时)。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。 回收器转换仅在以下情况下出现:在 Android 8.0 之前的低内存设备上,应用将进程状态从可察觉到暂停的状态(例如应用在前台运行时,这种情况下,用户可以察觉GC暂停)更改为察觉不到暂停的状态(反之亦然)。 |
HomogeneousSpaceCompact | 同构空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到察觉不到暂停的进程状态时发生。这样做的主要原因是减少内存使用量并对堆进行碎片整理。 |
DisableMovingGc | 这不是真正的GC原因,但请注意,由于在发生并发堆压缩时使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面存在限制。 |
HeapTrim | 这不是GC原因,但请注意,在堆修剪完成之前,回收会一直受到阻止。 |
GC名称表
名称 | 说明 |
---|---|
Concurrent mark sweep (CMS) | 整个堆回收器,会释放和回收除映像空间以外的所有其他空间。 |
Concurrent partial mark sweep | 几乎整个堆回收器,会回收除映像空间和 Zygote 空间以外的所有其他空间。 |
Concurrent sticky mark sweep | 分代回收器,只能释放自上次GC后分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,因为它更快速且暂停时间更短。 |
Marksweep + semispace | 非并发、复制 GC,用于堆转换以及同构空间压缩(对堆进行碎片整理)。 |
LMK机制
AKA,低内存杀手机制(Low Memory Killer),基于Linux内核的OOM Killer,指在系统内存不足时,系统开始依据自身的一套进程回收机制来判断要kill掉哪些进程,以腾出内存来供给需要的应用。
具体的判断指标叫oom_adj
,它是linux内核分配给每个系统进程的一个值,代表进程的优先级,进程回收机制就是根据这个优先级来决定是否进行回收:
- 进程的oom_adj越大,表示此进程优先级越低,越容易被杀回收;越小,表示进程优先级越高,越不容易被杀回收;
- 普通app进程的oom_adj >= 0,系统进程的oom_adj才可能 < 0;
oom_adh
是会动态改变的,比如进程在前台时可能为0,转为后台进程后变成正数,而且应用所占内存越大,进入后台后值越大,越容易被回收;
Android进程优先级划分:
优先级 | 进程类型 |
---|---|
1. | 前台进程(FOREGROUND_APP) |
2. | 可视进程(VISIBLE_APP) |
3. | 次要服务进程(SECONDARY_SERVER) |
4. | 后台进程 (HIDDEN_APP) |
5. | 内容供应节点(CONTENT_PROVIDER) |
6. | 空进程(EMPTY_APP) |
oom_adj
枚举值
枚举 | 数值 | 说明 | 内存阈值 |
---|---|---|---|
FOREGROUND_APP_ADJ | 0 | 前台进程,正在活动的Activity或者使用startForeground的Service | 1536(6M) |
VISIBLE_APP_ADJJ | 1 | 可见进程,不可操作的Activity,但是可见 | 2048(8M) |
SECONDARY_SERVER_ADJ | 2 | 拥有后台服务器的进程 | 4096(16M) |
HIDDEN_APP_MIN_ADJ | 7 | Activity没有完全退出,直接采用moveTaskToBack到HOME的进程 | 5120(20M) |
CONTENT_PROVIDER_ADJ | 14 | 内容提供进程 | 5632(22.4M) |
EMPTY_APP_AD | 15 | 空程序,既不提供服务,也不提供内容 | 6144(24M) |
CORE_SERVER_ADJ | 12 | 系统进程 | - |
SYSTEM_ADJ | -16 | 系统核心服务(进程永远不会被杀掉) | - |
数据来源:/init.rc
阈值单位是page分页,1page=4KB
查看进程oom_adj
的方法
# 获取指定的进程信息
ps | grep PackageName
# 通过PID获取进程oom_adj
cat /proc/PID/oom_adj
Android相关配置
<application
<!-- 申请大的虚拟机内存 -->
android:largeHeap="true"
<!-- 标记应用无法被杀死,但是必须是系统应用,即apk需放到/system/app下重启系统生效 -->
android:persistent="true"
/>