Android 内存管理

发表于我的博客网站(prajna.top): http://prajna.top/doc/4/189 欢迎大家前去交流。

 

这篇文章主要是解释 Android 是如何管理 app 进程和内存,低内存配置,和一些debug 手段, 本文的 android 版本是 4.4.

 

堆与栈

每个 proceess 都运行自己的虚拟内存空间里面, 对 32 位 OS 而言, 这个空间就是 4 G, 下面是一张linux process 的内存 layout.

 

Q1: Stack 和 Heap 是谁来管理的 ?

 

Q2: Stack 的大小是多少, 有限制吗 ?

 

Q3: Heap 的大小呢?

 

Java 虚拟机(JVM)

Java 运行时环境就是一个内存管理的环境, 主要就是管理前面提到的堆空间, 它逐步把这些内存交给malloc/dlmalloc 来管理. 它处理所有的内存分配,回收. Java 虚拟机实际上就是对堆进行管理.

 

 

由上图可以看到, JVM 的 HEAP 区域, 实际上划分为 2 个区域, Young Generation and Old Generation. Young 这个区域, 存放的是新创建的对象, 它的空间满了以后,GC 运行, 释放GC 运行后, 生命周期还没有结束的对象, 会被放到 Old 区域, 当 Old 区域满了以后, GC 再次运行. GC 在 Old 区域花费的时间, 远远大于 Young 区.

GC 运行的模式是 "Stop the World", 也就是 GC 运行时, 所有的线程都会停止.
Perm 其实不属于 java Heap 区域, 这个区域主要保存的是一些系统的东西, 如: runtime 类,静态变量等等.

 

堆的增长

JVM 的堆并不是一开始就分配最大的空间, 而是分配一个初始大小的空间. 所以, 它是动态变化的, 变化范围是 [heapstartsize .. heapsize]. 从上图可以看到, 当 Used 部分的内存达到当前堆的大小后 ,JVM 会做一次 GC, 无法满足的话, 就增加当前堆的大小, 上限就是 -Xmx 参数的值(heapsize).

Q: 在 Android 平台上, JVM 增大堆的策略是什么呢(成倍增加, 固定步进, ..)?

 

Android 如何管理内存

Android是基于Java的, 它同样也是利用JVM来管理app的内存分配. 但是, Android不提供交换分区(swap space), 不过,它支持 memory-mapping(*1), 你可以使用 mmap() 之类的 API 把文件映射到内存. 如果你修改了 map 文件对象, 该文件只能驻留内存,不能页面换出(page out), 这种情况下, 唯一释放内存的方法就在程序里面释放对象,让 java 的 GC 来回收; 如果 map 文件没有被修改 -- 如app 的 code, 那么可以从内存换出.

Davlik分配给 app 的堆空间会根据需要增加, 直到达到最大值, 系统会限制app所使用的最大堆空间.堆 空间的逻辑大小与物理大小是不同的. Android 不对堆做碎片化处理, 它不会把一些不再使用的空间合并 起来, Davlik 只是简单地检查堆空间,把不再使用的空间, 提交给 kernel.

 

共享内存和私有内存

每个app 进程运行被分配的当然都是私有内存, 因为, android 没有交换空间, 它不能被换出, 只有在进程销毁后,才能被回收. 除了私有内存, Android 系统还使用了共享内存, 共享内存包括: framework class, assets 和 native libraries. 每个 app 进程都是 forked Zygote 进程. Zygote 进程在系统启动的时候, 加载公用 framework code 和 资源文件(如 activity themes),以及一些 native 库– 这些构成了一个 app 进程运行的基本环境. 系统 fork zygote 进程, 在新的进程里面运行 app 的 code. 这样,分配给 framework 的代码和资源内存页面,就可以被 app 进程共用.

举个例子:

使用下面这个命令, 可以看到该进程的内存分配情况.

cat /proc/'pid'/maps

cat 某个 apk process :

53eb4000-53eb5000 r--p 00442000 b3:08 595 /system/framework/framework.odex

53eb5000-53eb9000 r--p 00443000 b3:08 595 /system/framework/framework.odex

53eb9000-53eba000 r--p 00447000 b3:08 595 /system/framework/framework.odex

53eba000-53ebb000 r--p 00448000 b3:08 595 /system/framework/framework.odex

53ebb000-53ebc000 r--p 00449000 b3:08 595 /system/framework/framework.odex

53ebc000-53ec5000 r--p 0044a000 b3:08 595 /system/framework/framework.odex

....

56e9a000-56ea0000 r-xp 00000000 b3:08 809 /system/lib/libdrmframework_jni.so

56ea0000-56ea1000 r--p 00005000 b3:08 809 /system/lib/libdrmframework_jni.so

56ea1000-56ea2000 rw-p 00006000 b3:08 809 /system/lib/libdrmframework_jni.so

56ea2000-56eb6000 r-xp 00000000 b3:08 808 /system/lib/libdrmframework.so

56eb6000-56eb9000 r--p 00013000 b3:08 808 /system/lib/libdrmframework.so

56eb9000-56eba000 rw-p 00016000 b3:08 808 /system/lib/libdrmframework.so

上面这些内存页面都是所有的 apk 共享的.

 

APP 内存限制及配置

为了维护多任务环境,Android 给每个 app 的内存堆空间设置了一个上限.如果,你的 app 已经达到上限,会收到OutOfMemoryError.你可以使用ActivityManager.getMemoryClass()来查询最大的内存空间.我查了下函数的实现,主要是对两个属性值: dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize, 如果都没有定义,默认最大为 16m.

 

 

在 Android 编译系统里面, 可以配置下列属性, 下面是一个例子.

dalvik.vm.heapstartsize=8m

dalvik.vm.heapgrowthlimit=64m

dalvik.vm.heapsize=256m

dalvik.vm.heaptargetutilization=0.75

dalvik.vm.heapminfree=512k

dalvik.vm.heapmaxfree=8m

 

dalvik.vm.heapstartsize (-Xms)

堆分配的初始大小,调整这个值会影响到应用的流畅性和整体 ram 消耗。这个值越小,系统 ram消耗越慢,但是由于初始值较小,一些较大的应用需要扩张这个堆,从而引发 GC 和堆调整的策略,会使应用反应更慢。相反,这个值越大系统 ram 消耗越快,但是程序更流畅。

dalvik.vm.heapgrowthlimit

每个app可以使用的最大内存,如果在AndroidManifest.xml中設定了android:largeHeap=true , 那么 growthlimit 实际无效,和 heapSize 相等了(见函数dvmClearGrowthLimit())

dalvik.vm.heapsize (-Xmx)

Java heap 的最大 size,他确定了 heap 创建时候 mmap 进来的内存的大小。对于应用程序来说,就是 heap 的上限。

dalvik.vm.heaptargetutilizatio,dalvik.vm.heapminfree,dalvik.vm.heapmaxfree

我们前面说到JVM对heap的管理,这 3 个 prop 就是和这个过程相关,3 个值一起设定了一个softlimit的值(见函数 setIdealFootprint())-- 也就是每次增长的大小,尽量去保证 heap 的使用率,让内存不会被浪费.前面提过了, 每次申请内存的时候,如果触及了 softlimit 首先会去做 gc, 在 gc 后还是无法申请成功的情况下,才去扩展 dlmalloc 管理的内存区域(见dvmHeapSourceAllocAndGrow())。

这 3 个 prop 的含义可以这么理解:

heaptargetutilization是当前dlmalloc管理的内存的理想的使用率(malloc_size/cur_heap_size) . 也就是说, 当前堆栈的大小, 与实际分配的内存是成比例的, 正常情况下, 设定为 0.75, 可以获得最优性能.Maxfree 当需要增大堆的时候, 最多增加(maxfree – curfree) 字节, 这是一个很重要的参数去调整GC的的工作频率. 这个值越小, 越容易触发 GC 运行. 而 GC 运行会影响到 app 的性能.

 

Q: 为什么 GC 运行会影响 app 性能?

 

下面是 HISI 平台的两个配置例子, lowmem 和 normal.

tablet-lowmem-dalvik-heap.mk

dalvik.vm.heapstartsize=5m

dalvik.vm.heapgrowthlimit=96m

dalvik.vm.heapsize=128m

dalvik.vm.heaptargetutilization=0.75

dalvik.vm.heapminfree=512k

dalvik.vm.heapmaxfree=2m

# Enable Low Ram Device flag

PRODUCT_PROPERTY_OVERRIDES += ro.config.low_ram=true

 

table-dalvik-heap.mk

dalvik.vm.heapstartsize=5m

dalvik.vm.heapgrowthlimit=128m

dalvik.vm.heapsize=512m

dalvik.vm.heaptargetutilization=0.75

dalvik.vm.heapminfree=512k

dalvik.vm.heapmaxfree=2m

 

从上面可以看出, 它们的差异主要在 heapgrowthlimit 和 heapsize. 如果一个比较耗 mem的app运行在 lowmem 的配置, 会不流畅, 卡顿, 甚至无法运行.

 

Low Ram Configuration

 

启动 Low Ram Device 标志

早期的 android 系统的内存比较小, 往往只有 256/512M, 为了兼容这些 low ram 设备,Android 4.4 引入了一支新的 API: ActivityManager.isLowRamDevice(), 使应用不仅能够检测是否是在低内存设备上运行,还能选择停用那些占用内存较大的功能。 启用该API,需要配置下面这个属性

PRODUCT_PROPERTY_OVERRIDES += ro.config.low_ram=true

 

停用 JIT

JIT 就是在运行的时候把字节码编译成机器码,并缓存它。如果将这一cache的上限设得过低,则会将 JIT 置于颠簸模式。对于内存确实很低的设备,Android 官方建议完全停用 JIT。 JIT 关掉后(每个进程最多可节省 1.5MB 的内存)。若要完全停用 JIT,请在产品 makefile 中添加下面这行内容:
PRODUCT_PROPERTY_OVERRIDES += dalvik.vm.jit.codecachesize=0

内核配置

微调内核 ActivityManager 以减少直接回收

当进程或内核尝试分配内存产生“缺页”故障时,内核已用尽所有可用内存时,就会发生直接回收。在这种情况下,内核将阻塞,等待磁盘 I/O 来清理页面 ,或 lowmemorykiller 终止一个进程。为避免出现直接回收,内核会在内存达到一个“临界点”的时候,提前通过kswapd 尝试释放页面。系统默认的阈值相当低 ,这即意味着,刚刚分配的内存会被马上回收,增加系统“颠簸”的概率。在 android-3.4 内核中,添加了可调整的接口,这些阈值可通过框架 config.xml 进行配置

/proc/sys/vm/extra_free_kbytes

-1

0

默认:reserve = displayWidth * displayHeight * 4 * 3 / 1024;

extraFreeKbytesAbsolute:非-1的情况, reserve = reserve_abs;

extraFreeKbytesAdjust: 非0情况下, reserve += reserve_adj;

 

微调 LowMemoryKiller

ActivityManager 可配置 LowMemoryKiller 的阈值,如果设备需要更多的服务(进程)并存,则可增大阈值。如果需要较多的内存,则可减小阈值——系统可以早一点杀死进程,释放内存。

 /sys/module/lowmemorykiller/parameters/minfree

/sys/module/lowmemorykiller/parameters/adj

 

-1

0

同理, config_.....Absolute 非-1,则系统直接使用该值。 config_....Adjust,在当前值上面增减。

交换到 zRAM
zRAM 交换可通过压缩内存页面并将其放入动态分配的内存交换区来增加系统中的可用内存量。但是,这会增加CPU 的负担。
Android 会在多个层面上处理 zRAM 交换:
  • 首先,必须启用以下内核选项:

    • CONFIG_SWAP

    • CONFIG_CGROUP_MEM_RES_CTLR

    • CONFIG_CGROUP_MEM_RES_CTLR_SWAP

    • CONFIG_ZRAM

  • 然后,您应将一行与下例类似的内容添加到您的 fstab 中:
    /dev/block/zram0  none  swap  defaults  zramsize=,swapprio=
    
    zramsize 是必要内容,表示您希望 zram 区域占用多少未压缩内存。压缩比通常为 30-50%。
    swapprio 是可选内容;如果您没有多个交换区,则无需使用此项。
    您还应确保在特定于设备的 sepolicy/file_contexts 中将关联的块设备标为 swap_block_device,以便 SELinux 适当地处理它。
    /dev/block/zram0 u:object_r:swap_block_device:s0
  • 默认情况下,Linux 内核每次会换入 8 页内存。当使用 ZRAM 时,由每次读取 1 页而产生的增量成本微乎其微,且可能有助于防止设备承受极端的内存压力。要想每次只读取 1 页,请将以下内容添加到您的 init.rc:
    write /proc/sys/vm/page-cluster 0

  • 在您的 init.rc 中的 mount_all /fstab. X 行后面,添加以下内容:
    swapon_all /fstab.X

  • 如果在内核中启用了此功能,系统便会在启动时自动配置内存 cgroup。

  • 如果内存 cgroup 可用,ActivityManager 就会将优先级较低的线程标为比其他线程更易于交换。如果需要内存,Android 内核则会开始将内存页面迁移到 zRAM 交换区,并会优先处理那些已被 ActivityManager 标记的内存页面。

 

Carveout、ION 和连续内存分配 (CMA)

对于低内存设备,需要特别注意 carveout,如果硬件允许不连续的内存分配,则 ION系统堆可从系统内存中分配内存,这样便无需使用 carveout。如果内存区域必须连续或必须限定在某个特定地址范围内,则可以使用连续内存分配器 (CMA)。

 

DEBUG 工具

1) 查看 App 使用多少内存 ?

adb shell dumpsys meminfo

Proportional Set Size (PSS)

这个值是 app 使用的所有内存大小, 私有内存 + 共享内存. 对共享内存, 它按照比例分配给每个进程.如:每个8K 的共享内存分配给了 2 个进程, 每个进程的 PSS 值就是 4k.

 

2) 查某个 app 的内存使用情况

adb shell dumpsys meminfo pid

Private Dirty(被 app 使用的内存, 没有缓存到磁盘) 是 app 的私有内存, 大部分的内存都被分配给私有内存.

Lost RAM: TotalRAM - FreeRAM - UsedRAM.

一般来说,kernel 使用的内存会在 Lost RAM里面,如果这个值特别大的话,很有可能是是被 ION 模块使用了,当然,也可能是内存泄漏,这种情况下,先Debug一下ION。

  • debugfs : /sys/kernel/debug/ion/
  • 依次检查下各个模块的内存大小,如:cat /sys/kernel/debug/ion/heaps/ion_system_heap

 

3) 查某个 app 一段时间内的内存使用状态

adb shell dumpsys procstats 'package name'

 

4) 使用 SIGQUIT 得到GC 性能信息

adb shell kill -S QUIT PID ,或者 busybox kill -3  PID

上面命令会产生 /data/anr/traces.txt 文件,这个文件包含GC的timing 信息。搜索"Dumping cumulative Gc timings",会有GC周期内的暂停的时间信息

sticky concurrent mark sweep paused: Sum: 5.491ms 99% C.I. 1.464ms-2.133ms Avg: 1.830ms Max: 2.133ms

平均暂停时间是 1.830ms,性能还是不错的。 suspend也是一个值得的关注点,

suspend all histogram: Sum: 1.513ms 99% C.I. 3us-546.560us Avg: 47.281us Max: 601us

当GC请求一个线程中止的时候,从请求到中止总共花费了的时间,也是包含在 GC 暂停的时间里面的。如果,GC 暂停的时间过长,会导致线程中止变得很慢。

还有下面几个数据,也值得关注。

Total time spent in GC: 502.251ms
Mean GC size throughput: 92MB/s
Mean GC object throughput: 1.54702e+06 objects/s

 

Q&A

Q1: Stack 和 Heap 是谁来管理的 ?

A: Stack主要是编译工具管理,而Heap 则主要是靠应用程序来管理。

代码编译的时候,编译工具会把局部变量和函数地址,都放入堆栈,特别是涉及函数调用的地方,会使用用大量的push/pop指令来执行的入栈,出栈的操作。 Heap空间完全由应用程序来管理,kernel只负责具体的分配和释放,需不需要释放,什么时候释放,完全由应用程序来控制。

Q2 : Stack 的大小是多少 ?

A: ulimit -a

stack size (kbytes, -s) 8192
堆栈的大小一般默认是 8M,你可以使用API 进行调整。

Q3: Heap 的大小呢?

A: Stack 大小确定后,再减去全局数据区域,代码区域等, 剩余的空间都是给可以给到Heap 了。

 

(*1) Memory-mapping 是一种虚拟内存管理, 把物理文件映射到内存.

 

参考文章:

1: 官方开发文档: "Managing Your App's Memory"

2: JournalDEV: "Java (JVM) Memory Model – Memory Management in Java"

你可能感兴趣的:(Android,Android)