Android内存优化理论基础及相关工具

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_ReasonGC原因,如下表所示。
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_ReasonGC原因,如下表所示。
GC_NameGC名称,如下表所示。
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_ADJ0前台进程,正在活动的Activity或者使用startForeground的Service1536(6M)
VISIBLE_APP_ADJJ1可见进程,不可操作的Activity,但是可见2048(8M)
SECONDARY_SERVER_ADJ2拥有后台服务器的进程4096(16M)
HIDDEN_APP_MIN_ADJ7Activity没有完全退出,直接采用moveTaskToBack到HOME的进程5120(20M)
CONTENT_PROVIDER_ADJ14内容提供进程5632(22.4M)
EMPTY_APP_AD15空程序,既不提供服务,也不提供内容6144(24M)
CORE_SERVER_ADJ12系统进程-
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"
/>

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值