在上一章,咱们现已从操作体系的维度了解了一个进程的内存模型。这一节,咱们将维度持续上升,从运用层出发看看一个 App 运行时的内存模型是怎样的。从 App 运行时的内存模型中咱们能够知道导致内存增长的源头,从源头出发,能够更有目的去管理内存,还能进一步剖析引起增长的代码逻辑或者数据。
为了让咱们深化把握 App 运行时的内存模型,这一节的内容按照由外到内、逐渐深化的准则,分为了 3 个部分:
-
内存描绘方针
-
内存数据获取
-
内存模型详解
话不多说,让咱们立刻开端这一章学习吧!
内存描绘方针
在进行内存优化之前,咱们必须要先了解常用的内存描绘方针。内存描绘方针能够用来度量一个 App 的内存状况,也能够在咱们做内存优化时,更直观地展示出优化前后的效果。
常用的内存描绘方针有 6 个,咱们先来简单了解一下。
-
PSS( Proportional Set Size ):实践运用的物理内存,会按份额分配同享的内存。比如一个运用有两个进程都用到了 Chrome 的 V8 引擎,那么每个进程会承担 50% 的 V8 这个 so 库的内存占用。PSS 是咱们运用最频频的一个方针,App 线上的内存数据核算一般都取这个方针。
-
RSS( Resident Set Size ):PSS 中的同享库会按份额分管,可是 RSS 不会,它会彻底算进当时进程,所以把所有进程的 RSS 加总后得出来的内存会比实践高。按份额核算内存占用会有一定的耗费,因此当想要高功能的获取内存数据时便能够运用 RSS,Android 的 LowMemoryKiller 机制就是根据每个进程的 RSS 来核算进程优先级的。
-
Private Clean / Private Dirty:当咱们履行 dump meminfo 时会看到这个方针,Private 内存是只被当时进程独占的物理内存。独占的意思是即便释放之后也无法被其他进程运用,只要当这个进程销毁后其他进程才干运用。Clean 表明该对应的物理内存现已释放了,Dirty 表明对应的物理内存还在运用。
-
Swap Pss Dirty:这个方针和上面的 Private 方针刚好相反,Swap 的内存被释放后,其他进程也能够持续运用,所以咱们在 meminfo 中只看得到 Swap Pss Dirty,而看不到Swap Pss Clean,由于 Swap Pss Clean 是没有意义的。
-
Heap Alloc:经过 Malloc、mmap 等函数实践请求的虚拟内存,包含 Naitve 和虚拟机请求的内存。
-
Heap Free:闲暇的虚拟内存。
内存描绘方针并不多,上面这几个就彻底够用了,而且我相信咱们或多或少都触摸过,所以这儿列出来便于咱们后面查阅。
内存数据获取
了解了内存的描绘方针,咱们再来看看怎么获取内存的数据,主要有 2 种办法。
① 线下经过 adb 指令获取,一般用于线下调试:
adb shell
dumpsys meminfo 进程名/pid
② 线上经过代码获取,一般用于收集线上的内存数据:
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
尽管获取办法不同,但这两种办法获取数据的原理彻底相同,它们调用的都是 android_os_Debug.cpp 目标中的 android_os_Debug_getDirtyPagesPid
接口,它的源码如下:
static jboolean android_os_Debug_getDirtyPagesPid(JNIEnv *env, jobject clazz,
jint pid, jobject object)
{
bool foundSwapPss;
stats_t stats[_NUM_HEAP];
memset(&stats, 0, sizeof(stats));
//1. 加载maps文件,获取
if (!load_maps(pid, stats, &foundSwapPss)) {
return JNI_FALSE;
}
struct graphics_memory_pss graphics_mem;
//2. 获取graphics区域内存数据
if (read_memtrack_memory(pid, &graphics_mem) == 0) {
stats[HEAP_GRAPHICS].pss = graphics_mem.graphics;
stats[HEAP_GRAPHICS].privateDirty = graphics_mem.graphics;
stats[HEAP_GRAPHICS].rss = graphics_mem.graphics;
stats[HEAP_GL].pss = graphics_mem.gl;
stats[HEAP_GL].privateDirty = graphics_mem.gl;
stats[HEAP_GL].rss = graphics_mem.gl;
stats[HEAP_OTHER_MEMTRACK].pss = graphics_mem.other;
stats[HEAP_OTHER_MEMTRACK].privateDirty = graphics_mem.other;
stats[HEAP_OTHER_MEMTRACK].rss = graphics_mem.other;
}
//3. 获取Unkonw区域数据
for (int i=_NUM_CORE_HEAP; i<_NUM_EXCLUSIVE_HEAP; i++) {
stats[HEAP_UNKNOWN].pss += stats[i].pss;
stats[HEAP_UNKNOWN].swappablePss += stats[i].swappablePss;
stats[HEAP_UNKNOWN].rss += stats[i].rss;
stats[HEAP_UNKNOWN].privateDirty += stats[i].privateDirty;
stats[HEAP_UNKNOWN].sharedDirty += stats[i].sharedDirty;
stats[HEAP_UNKNOWN].privateClean += stats[i].privateClean;
stats[HEAP_UNKNOWN].sharedClean += stats[i].sharedClean;
stats[HEAP_UNKNOWN].swappedOut += stats[i].swappedOut;
stats[HEAP_UNKNOWN].swappedOutPss += stats[i].swappedOutPss;
}
//4. 将获取的数据寄存到容器中
……
return JNI_TRUE;
}
这段源码比较长,咱们一起来梳理下里面的逻辑,主要分为 4 部分。
-
读取 maps 文件,获取该进程的内存详情:经过上一节的学习,咱们知道进程运用的内存都是虚拟内存,而且虚拟内存都以页为维度来办理和维护。这个进程的虚拟内存每一页上寄存了什么数据,都会记录在 maps 文件中,maps 文件是一个很重要的文件,后面会具体介绍它。
-
调用 libmemtrack 接口获取 graphics 内存数据:Graphic 内存分配和运用办法具有特殊性,并没有悉数映射到运用进程,需求经过 HAL 层(抽象硬件层)libmemtrack 的接口查询,才干完整得到运用的 graphics 内存数据。
-
分配 Unknow 区域的内存数据:根据前面的知识咱们知道,mmap 除了做内存映射,还能够用来请求虚拟内存,假如在请求内存时是私有且匿名的( fd 假如为 -1,flag 入参为MAP_ANONYMOUS 或 MAP_PRIVATE )就会算入 Unknow 中,假如 mmap 请求内存时指定了请求这段内存的名字,就会算入 Other Dev 当中。因此,对这一区域内存问题的排查往往比较复杂,由于咱们不知道内存的来历。
-
寄存获取到的内存数据并回来:最后一部分就是将前面获取到的数据放到对应的数据结构中,并回来给接口调用方。
内存模型详解
咱们现已知道怎么获取内存数据,可是这些数据从哪儿来呢?究竟只要知道来历,咱们才干从源头进行管理。那接下来,咱们就对 App 运行时的内存模型进行一个全面且具体的剖析。
咱们以体系设置这个 App 为比如,经过 adb 指令获取的内存数据如下:
这儿把上面的数据分为两个部分:A 区域和 B 区域。其间 A 区域的数据主要来自前面说到的 android_os_Debug_getMemInfo
接口,B 区域的数据则是对 A 区域中的数据做了汇总处理。
A区域
前面咱们现已了解到,android_os_Debug_getMemInfo 接口的数据有两部分来历,一部分是读取 maps 文件解析到每块内存所属的数据,另一部分是读取 libmemtrack 接口的数据获取到的 graphic 内存数据。这两部分的数据来历就组成了 A 区域中的三块数据。下面咱们别离来看看这三块数据。
数据 ①:maps 文件数据
maps 文件是剖析内存很重要的一个文件,经过 maps 文件咱们能够具体知道这个进程的内存中寄存了哪些数据。maps 文件寄存在 /proc/{ pid }/maps 途径中,该途径除了寄存该进程的 maps 文件,还寄存了该进程的所有其他信息的数据。假如你感兴趣能够深化了解一下。
关于 root 的手机,咱们能够直接检查该目录下的 maps 文件。可是 maps 文件十分长,直接看会很吃力,所以咱们一般会经过脚本对 maps 文件中的数据做剖析和归类。下面还是以体系设置这个运用为例,它的 maps 文件的部分内容如下:
图中从左至右各个数据段的解说如下:
字段 | address | perms offset | offset | dev | inode | pathname |
---|---|---|---|---|---|---|
数据 | 12c00000-32c00000 | rw-p | 00000000 | 00:00 | 0 | main space (region space)] |
意义 | 本段内存映射的虚拟地址空间范围 | 读写权限 | 本段映射地址在文件中的偏移 | 所映射的文件所属设备的设备号 | 文件的索引节点号 | 对有名映射而言,pathname 是映射的文件名;对匿名映射来说,pathname 是此段内存在进程中的作用 |
假如手机没有 root 也不要紧,咱们能够在运行时经过 native 层的 c++ 代码读取该文件,能够看一下android_os_Debug_getMemInfo
接口中调用的 load_maps 办法,该办法读取 maps 文件后,还做了一个具体的分类操作,分完类之后就是咱们看到的数据 ① 中的数据,这个办法比较长,所以我精简了部分代码。
static bool load_maps(int pid, stats_t* stats, bool* foundSwapPss)
{
*foundSwapPss = false;
uint64_t prev_end = 0;
int prev_heap = HEAP_UNKNOWN;
std::string smaps_path = base::StringPrintf("/proc/%d/smaps", pid);
auto vma_scan = [&](const meminfo::Vma& vma) {
int which_heap = HEAP_UNKNOWN;
int sub_heap = HEAP_UNKNOWN;
bool is_swappable = false;
std::string name;
if (base::EndsWith(vma.name, " (deleted)")) {
name = vma.name.substr(0, vma.name.size() - strlen(" (deleted)"));
} else {
name = vma.name;
}
uint32_t namesz = name.size();
// 解析Native Heap 内存
if (base::StartsWith(name, "[heap]")) {
which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:libc_malloc]")) {
which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:scudo:")) {
which_heap = HEAP_NATIVE;
} else if (base::StartsWith(name, "[anon:GWP-ASan")) {
which_heap = HEAP_NATIVE;
}
// 解析 stack 部分内存
else if (base::StartsWith(name, "[stack")) {
which_heap = HEAP_STACK;
} else if (base::StartsWith(name, "[anon:stack_and_tls:")) {
which_heap = HEAP_STACK;
}
// 解析 code 部分的内存
else if (base::EndsWith(name, ".so")) {
which_heap = HEAP_SO;
is_swappable = true;
} else if (base::EndsWith(name, ".jar")) {
which_heap = HEAP_JAR;
is_swappable = true;
} else if (base::EndsWith(name, ".apk")) {
which_heap = HEAP_APK;
is_swappable = true;
} else if (base::EndsWith(name, ".ttf")) {
which_heap = HEAP_TTF;
is_swappable = true;
} else if ((base::EndsWith(name, ".odex")) ||
(namesz > 4 && strstr(name.c_str(), ".dex") != nullptr)) {
which_heap = HEAP_DEX;
sub_heap = HEAP_DEX_APP_DEX;
is_swappable = true;
} else if (base::EndsWith(name, ".vdex")) {
which_heap = HEAP_DEX;
……
} else if (base::EndsWith(name, ".oat")) {
which_heap = HEAP_OAT;
is_swappable = true;
} else if (base::EndsWith(name, ".art") || base::EndsWith(name, ".art]")) {
which_heap = HEAP_ART;
……
} else if (base::StartsWith(name, "/dev/")) {
which_heap = HEAP_UNKNOWN_DEV;
// 解析 gl 区域内存
if (base::StartsWith(name, "/dev/kgsl-3d0")) {
which_heap = HEAP_GL_DEV;
}
// 解析 cursor 区域内存
else if (base::StartsWith(name, "/dev/ashmem/CursorWindow")) {
which_heap = HEAP_CURSOR;
} else if (base::StartsWith(name, "/dev/ashmem/jit-zygote-cache")) {
which_heap = HEAP_DALVIK_OTHER;
sub_heap = HEAP_DALVIK_OTHER_ZYGOTE_CODE_CACHE;
}
//解析ashmen匿名同享内存
else if (base::StartsWith(name, "/dev/ashmem")) {
which_heap = HEAP_ASHMEM;
}
} else if (base::StartsWith(name, "/memfd:jit-cache")) {
which_heap = HEAP_DALVIK_OTHER;
sub_heap = HEAP_DALVIK_OTHER_APP_CODE_CACHE;
} else if (base::StartsWith(name, "/memfd:jit-zygote-cache")) {
which_heap = HEAP_DALVIK_OTHER;
sub_heap = HEAP_DALVIK_OTHER_ZYGOTE_CODE_CACHE;
}
//解析java Heap内存
else if (base::StartsWith(name, "[anon:")) {
which_heap = HEAP_UNKNOWN;
if (base::StartsWith(name, "[anon:dalvik-")) {
which_heap = HEAP_DALVIK_OTHER;
if (base::StartsWith(name, "[anon:dalvik-LinearAlloc")) {
sub_heap = HEAP_DALVIK_OTHER_LINEARALLOC;
} else if (base::StartsWith(name, "[anon:dalvik-alloc space") ||
base::StartsWith(name, "[anon:dalvik-main space")) {
// This is the regular Dalvik heap.
which_heap = HEAP_DALVIK;
sub_heap = HEAP_DALVIK_NORMAL;
} else if (base::StartsWith(name,
"[anon:dalvik-large object space") ||
base::StartsWith(
name, "[anon:dalvik-free list large object space")) {
which_heap = HEAP_DALVIK;
sub_heap = HEAP_DALVIK_LARGE;
} else if (base::StartsWith(name, "[anon:dalvik-non moving space")) {
which_heap = HEAP_DALVIK;
sub_heap = HEAP_DALVIK_NON_MOVING;
} else if (base::StartsWith(name, "[anon:dalvik-zygote space")) {
which_heap = HEAP_DALVIK;
sub_heap = HEAP_DALVIK_ZYGOTE;
} else if (base::StartsWith(name, "[anon:dalvik-indirect ref")) {
sub_heap = HEAP_DALVIK_OTHER_INDIRECT_REFERENCE_TABLE;
} else if (base::StartsWith(name, "[anon:dalvik-jit-code-cache") ||
base::StartsWith(name, "[anon:dalvik-data-code-cache")) {
sub_heap = HEAP_DALVIK_OTHER_APP_CODE_CACHE;
} else if (base::StartsWith(name, "[anon:dalvik-CompilerMetadata")) {
sub_heap = HEAP_DALVIK_OTHER_COMPILER_METADATA;
} else {
sub_heap = HEAP_DALVIK_OTHER_ACCOUNTING; // Default to accounting.
}
}
} else if (namesz > 0) {
which_heap = HEAP_UNKNOWN_MAP;
} else if (vma.start == prev_end && prev_heap == HEAP_SO) {
// bss section of a shared library
which_heap = HEAP_SO;
}
prev_end = vma.end;
prev_heap = which_heap;
const meminfo::MemUsage& usage = vma.usage;
if (usage.swap_pss > 0 && *foundSwapPss != true) {
*foundSwapPss = true;
}
uint64_t swapable_pss = 0;
if (is_swappable && (usage.pss > 0)) {
float sharing_proportion = 0.0;
if ((usage.shared_clean > 0) || (usage.shared_dirty > 0)) {
sharing_proportion = (usage.pss - usage.uss) / (usage.shared_clean + usage.shared_dirty);
}
swapable_pss = (sharing_proportion * usage.shared_clean) + usage.private_clean;
}
// 将获取的数据进行累加
……
};
//for循环函数,履行maps文件的读取
return meminfo::ForEachVmaFromFile(smaps_path, vma_scan);
}
经过上面临 maps 的解析函数,咱们不只能够看到 maps 中的数据类型及格式,也能够知道 Dalvik Heap,Native Heap 等数据的组成。在做内存的线上反常监控时,反常状况下,也能够将 maps 文件上传到服务端,服务端对 maps 文件进行解析和分类,这样咱们就能十分便利的定位和排查线上内存问题。
数据②:graphic 相关数据
了解了 maps 文件中的内存数据,咱们再来看看 graphic 的数据,graphic 的数据有 3 部分。
-
Gfx dev:制作时分配,而且现已映射到运用进程虚拟内存中。这儿需求留意的是,只要高通的芯片才会将这一块的内寄存在 /dev/kgsl-3d0 途径,并映射到进程的虚拟内存中,其他的芯片不会放在这个途径。在上面的 load_maps 办法中,咱们也能够看到对这一块内存数据的解析逻辑。
-
GL mtrack:制作时分配,没有映射到运用地址空间,包含纹理、顶点数据、shader program 等。
-
EGL mtrack:运用的 Layer Surface,经过 gralloc 分配,没有映射到运用地址空间。不了解 Layer Surface 的话,能够将一个界面理解成一个 Layer Surface,Surface 存储了界面的数据,并交给 GPU 制作。
上面 1 的数据是经过 load_maps 函数解析获取的,2 和 3 的数据是经过 read_memtrack_memory 函数获取的。该函数会读取和解析途径为 /d/kgsl/proc/{ pid }/mem 的文件,这个文件节点中的数据是gpu driver写入的,该办法的完成能够参考下面高通855源码中的 kgsl_memtrack_get_memory 函数,下面是这个函数的主体逻辑代码。(官方源码:kgsl.c)
int kgsl_memtrack_get_memory(pid_t pid, enum memtrack_type type,
struct memtrack_record *records,
size_t *num_records)
{
……
// 1. 设置方针文件途径
snprintf(tmp, sizeof(tmp), "/d/kgsl/proc/%d/mem", pid);
……
while (1) {
// 2. 读取并解析该文件
……
}
……
return 0;
}
咱们也能够在 root 手机中,检查 kgsl_memtrack_get_memory
函数读取到该运用进程的数据,下面是体系设置这个运用的部分 graphic 数据。
/d/kgsl/proc/3160 # cat mem
gpuaddr useraddr size id flags type usage sglen mapcount eglsrf eglimg
0000000000000000 0 196608 1 --w---N-- gpumem any(0) 0 0 0 0
0000000000000000 0 16384 2 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 4096 3 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 4 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 5 --w--pY-- gpumem gl 0 1 0 0
0000000000000000 0 4096 6 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 7 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 20480 8 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 9 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 10 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 196608 11 --w---N-- gpumem any(0) 0 0 0 0
0000000000000000 0 16384 12 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 4096 13 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 14 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 15 --w--pY-- gpumem gl 0 1 0 0
0000000000000000 0 4096 16 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 17 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 32768 18 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 19 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 20 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 65536 21 --w--pY-- gpumem arraybuffer 0 1 0 0
0000000000000000 0 131072 22 --wl-pY-- gpumem command 0 1 0 0
0000000000000000 0 32768 23 --w--pY-- gpumem gl 0 1 0 0
0000000000000000 0 131072 24 --wl-pY-- gpumem gl 0 1 0 0
0000000000000000 0 8192 25 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 8192 26 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 16384 27 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 9469952 28 --wL--N-- ion egl_surface 152 0 1 1
0000000000000000 0 131072 29 --wl-pY-- gpumem command 0 1 0 0
0000000000000000 0 8192 30 --w--pY-- gpumem command 0 1 0 0
0000000000000000 0 4096 31 -----pY-- gpumem gl 0 1 0 0
0000000000000000 0 4096 32 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 33 -----pY-- gpumem gl 0 1 0 0
0000000000000000 0 4096 34 --w--pY-- gpumem any(0) 0 1 0 0
0000000000000000 0 4096 35 -----pY-- gpumem gl 0 1 0 0
……
数据③:Alloc 内存
在内存描绘方针这一部分,咱们现已知道数据 ③ 中的数据是调用 malloc、mmap、calloc 等内存请求函数时积累的数据,想要获取这个数据,能够经过下面的接口完成。
- 获取 Java 层请求的内存:会直接去 Art 虚拟机中获取虚拟机现已请求的内存大小。
Runtime runtime = Runtime.getRuntime();
//获取现已请求的Java内存 long usedMemory=runtime.totalMemory() ;
//获取请求但未运用Java内存 long freeMemory = runtime.freeMemory();
- 获取 Native 请求的内存:会调用 android_os_Debug.cpp 目标中的android_os_Debug_getNativeHeapSize 接口获取数据,该接口又是调用的 mallinfo 函数,mallinfo 函数会回来 native 层现已请求的内存大小。
//获取现已请求的Native内存
long nativeHeapSize = Debug.getNativeHeapSize()
//获取请求但未运用Native内存
long nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
//Naitve层
static jlong android_os_Debug_getNativeHeapSize(JNIEnv *env, jobject clazz)
{
struct mallinfo info = mallinfo();
return (jlong) info.usmblks;
}
咱们能够看下 mallinfo 函数的说明文档:
经过上面两个接口获取 Naitve 和 Java 的内存数据效率最高,功能耗费最小,所以适合在代码中做数据监控运用。经过读取和解析 maps 文件来获取内存数据对功能的开销较大,所以从 Android10 开端加了 5 分钟的频控。
B区域
B 区域的数据就是将 A 区域中的 ① 数据做了汇总操作,便利咱们检查,并没有太特别的内容,这儿就简单列一下了。
-
Java Heap:(Dalvik Heap 的 Private Dirty 数据) + ( .art mmap 部分的 Private Dirty 和 Private Clean 数据) + getOtherPrivate ( OTHER_ART ) 。这儿的 .art 是运用的 dex 文件预编译后的 art 文件,所以也是属于该运用的 JavaHeap。
-
Native Heap:Native Heap 的 Private Dirty 数据。
-
Code:.so .jar .apk .ttf .dex .oat 等资源加总。
-
Stack:getOtherPrivateDirty ( OTHER_STACK )。
-
Graphics:gl,gfx,egl 的数据加总。
-
System:( Total Pss ) – ( Private Dirty 和 Private Clean 的总和)。主要是体系占用的内存,如同享的字体、图像资源等。
小结
想要深化把握 App 运行时的内存模型,夯实内存优化的基础,首要咱们要了解描绘内存的方针,它们是度量咱们内存优化效果的重要工具。
常用的方针有 6 个,别离是同享库按份额分管的 Pss;进程在 RAM 中实践保存的总内存 RSS;只被当时进程独占的物理内存 Private Clean / Private Dirty;和 Private 相反的 Swap Pss Dirty;以及 Heap Alloc 和闲暇的虚拟内存 Heap Free。获取这些方针的办法有两个,线下能够经过 adb 指令获取,线上能够经过代码获取。
其次,咱们需求从原理上深化了解内存的组成,以及这些组成的来历,这样咱们才干在内存优化中,做到有的放矢。咱们要点把握 3 类数据:maps 文件数据、graphic 相关数据和 Alloc 内存。
这一章节的内容尽管属于基础知识,但把握它们能够在后面的实战章节中,帮助咱们更简单理解和上手。