编辑推荐: |
本文主要介绍
了Android 内存管理相关知识。需要的朋友们下面来一起学习学习吧!
本文来自于博客园,由Linda编辑、推荐。 |
|
一、需求
我司存在内存为1G RAM的设备,属于低内存设备,经常会出现内存很紧张的场景,也容易因此导致一系列七七八八的边际问题,故有必要了解Android系统的内存相关知识:
了解内存的分配、回收方式
了解OOM、LMK的相关机制
了解Android系统内存相关调试方式
了解Android系统的性能优化方案
二、环境
JDK 1.8
Android 10
三、JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一个虚构出来的计算机,有着自己完善的硬件架构,如处理器、堆栈等。
3.1 编译&执行过程
Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
Java文件必须先通过一个叫javac的编译器,将代码编译成class文件,然后通过JVM把class文件解释成各个平台可以识别的机器码,最终实现跨平台运行代码。
3.2 JVM内存模型
3.2.1 方法区
方法区是《Java虚拟机规范》中规定的一个内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。方法区是一个规范,它的实现取决于不同的虚拟机:
在Java8之前,HotSpot虚拟机使用 永久代 来实现方法区。
而Java8之后,HotSpot虚拟机使用 元空间 来实现方法区。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存;永久代在虚拟机中。
方法区存储的信息如下:
名称 |
描述 |
类型信息 |
(1)是类class、接口interface、枚举enum、注解annotation中哪一种
(2)完整有效名称(包名.类名)
(3)直接父类的完整名称(接口和java.lang.Object没有父类)
(4)类型的修饰符(public、abstract、final等)
(5)类型直接接口的有序列表(实现的接口构成列表) |
域(Field、属性)信息 |
(1)保存类型所有域(属性)的相关信息和声明顺序
(2)相关信息包含:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient等) |
方法(method)信息:按顺序保存 |
(1)方法名称
(2)返回类型(含Void)
(3)方法参数和类型(按顺序)
(4)方法的修饰符(public、private、protected、static、final、synchronized、native、abstract)
(5)方法的字节码、操作数栈、局部变量表及其大小(abstract和native方法除外)
(6)异常表abstract和native方法除外),每个异常处理开始、结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引等 |
Non-final类变量:(static修饰的变量,静态变量) | (1)逻辑上是类数据一部分
(2)在类的加载过程中链接的准备阶段设置默认初始值,初始化阶段赋予真实值
(3)类变量(non-final)被所有实例共享,没有实例化类对象也可访问,(全局常量,static和final一起修饰)
(4)与final修饰的类变量不同,每个全局常量在编译时就分配了 |
Class文件常量池 | (1)一个有效的字节码文件除了包含类的版本信息、字段、方法以及接口描述信息外,还包含一个常量池表。常量池表中包含字面量、域和方法的符号引用。
(2)字面量就是int i=5;String=”Hello World!”中的5和”Hello
World!”
(3)一个JAVA源原文件中的类、接口,编译后生成字节码文件,Java中的字节码需要数据,但是这些数据很多很大,不能直接存到内存中,可以将其存到常量池中,字节码中包含了指向常量池的引用。
(4)常量池中包含:数量值、字符串引用、类引用、字段引用、方法引用 |
运行时常量池 | (1)运行时常量池是方法区的一部分
(2)常量池表示Class中的一部分,用于存放编译器生成的各种字面量和符号引用,在加载类和接口到虚拟机后,就创建相应的运行时常量池
(3)JVM为每个加载的类或接口维护一个运行时常量池,池中数据类似数组项,通过索引访问
(4)运行时常量池中含多种不同常量,包含编译器就明确的数值字面量,也包含运行期的方法或者字段引用,此时不再是常量池中的符号地址,而是真实地址。
(5)运行时常量池,相对于Class文件中的常量池,还有一个特征就是具备动态性,可以动态添加数据到运行时常量池
(6)当创建运行时常量池时,如果所需内存空间大于方法区能提供的最大值,那么JVM抛出OutOfMemoryError异常 |
3.2.2 堆
堆是java内存管理中最大的一块内存,也是所有线程共享的一块内存,在虚拟机启动时创建。堆中主要存放的是对象实例、数组。几乎所有的对象实例、数组都在这一块内存中分配。
堆也是GC垃圾回收的主要区域。垃圾回收现在主要采取的是分代垃圾回收算法。为了方便垃圾回收,java堆还进行了细分:新生代(YoungGen)、和老年代(oldGen),默认占比为:1:2;其中新生代还可以划分为Eden空间、survivor0空间、survivor1空间,默认占比为:8:1:1;
对象内存分配过程如下:
1.new一个对象value,value先放于新生代->Eden区;
2.当Eden区空间填满后,我们需要再创建value2对象,JVM会对Eden区继续垃圾回收(Minor
GC);
3.Eden区触发GC后,Eden区会被清空,同时Eden区幸存对象会移动到S0幸存区。此时,Eden区和S1区未存放对象;
4.如果Eden区再次被填满,再次触发GC,此时会对Eden区和S0区进行垃圾回收,存活对象移动至S1幸存区。此时Eden区和S0区未存放对象;
5.在eden区发生gc后剩余对象内存大于s区时,直接进入老年代。
6.如果再次经历垃圾回收,此时幸存对象会重新放回S0区,如此反复,幸存区会永远存在一个区为空对象;
7. 当我们的对象时长超过一定年龄时(默认15,可以通过参数设置),将会把对象放入老生代,当然大的对象会直接进入老生代。老生代采用的回收算法是标记整理算法。
8. 当老年代内存满了或者发生young GC后要转移至老年代的对象内存大于老年代剩余内存时,触发Full
GC(Full GC会触发STW(stop the world))。
3.2.3 程序计数器
程序计数器是一块较小的内存空间,可看作是当前线程所执行字节码的行号指示器。字节码解释器根据这个计数器来获取当前线程需要执行的下一条指令,分支、循环、跳转、异常、线程恢复等功能都需要依赖程序计数器来完成。
此外,在线程争夺CPU时间片的时候,需要线程切换,这时候,就需要这个计数器来帮助线程恢复到正确执行的位置,每一条线程有自己的程序计数器,所以才能够保证当前程序能够正确恢复到上次执行的步骤。
ps:程序计数器是唯一一个不会出现OOM错误的内存区域,它的生命周期伴随线程的创建而创建,随程序的消亡而消亡。
3.2.4 虚拟机栈
虚拟机栈是线程私有的。虚拟机栈跟线程的生命周期相同,它描述的是java方法执行的内存模型,每次java方法调用的数据,都是通过栈传递的。
java内存可以粗糙的分为 堆内存(heap)和 栈内存(stack) ,其中栈内存就是指的虚拟机栈,或者说是虚拟机栈中局部变量表中的部分。实际上,虚拟机栈就是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息:
名称 |
内容 |
局部变量表 |
主要存放的是编译期间可知的各种数据类型(八大基本数据类型)、对象引用(Reference类型,不同于对象,可能是指向对象地址的指针或者与此对象位置相关的信息) |
操作数栈 |
主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间 |
动态链接 |
对运行常量池的引用,类加载机制过程中解析那一步的作用是将常量池中的符号引用替换成直接引用,这叫静态链接,而这里的动态连接的意思是在运行过程中转换成直接引用 |
方法出口 |
无论是程序正常返回或者是异常调用完成返回,都必须回到最初方法被调用时的位置。 |
虚拟机栈可能抛出两种错误:StackOverflowError 、OutOfMemoryError。
3.2.5 本地方法栈
本地方法栈的工作原理跟虚拟机栈并无区别,唯一的区别就是本地方法栈面向的不是.class字节码,而是Native修饰的本地方法。
本地方法的执行过程,也是本地方法栈中栈帧的出栈过程。
同虚拟机栈一样,本地方法栈也是会抛出 StackOverflowError 、OutOfMemoryError
两种异常。
3.2.6 直接内存
直接内存有一种叫法,堆外内存。
直接内存(堆外内存)指的是Java应用程序通过直接方式从操作系统中申请的内存。这个的差别与之前的堆,栈,方法区不同。那些内存都是经过了虚拟化的内存
3.2.7 方法区、堆、栈之间的关系
栈中的类字节码存储在方法区(也就是存类),实例化对象存储在Java堆,对象引用存储在栈中。
四、OOM
OOM(Out of Memory)即内存溢出,是因为应用所需要分配的内存超过系统对应用内存的阈值,而抛出的
java.lang.OutOfMemoryError错误。 OOM的根本原因是开发者对内存使用不当造成的。
Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,也就是说每个应用程序都是在属于自己的进程中运行的。如果程序内存溢出,Android系统只会kill掉该进程,而不会影响其他进程的使用(如果是system_process等系统进程出问题的话,则会引起系统重启)。
4.1 OOM阈值
Android系统JVM对应用所分配的内存阈值:
sl8541e_1h10_32b:/ # getprop | grep dalvik.vm.heap
[dalvik.vm.heapgrowthlimit]: [80m]
[dalvik.vm.heapsize]: [256m]
[dalvik.vm.heapstartsize]: [6m]
|
4.2 OOM演示
4.2.1 测试代码
void testForOutMemory(){
ActivityManager mActivityManager = (ActivityManager) getApplication().getSystemService(Context.ACTIVITY_SERVICE);
int largeMemoryClass = mActivityManager.getLargeMemoryClass();
int memoryClass = mActivityManager.getMemoryClass();
int currentMemory = (int) Runtime.getRuntime().maxMemory() /1024 /1024;
Log.d("LZQ","[show memory] largeMemoryClass = " + largeMemoryClass + " | memoryClass = " + memoryClass + " | currentMemory = " + currentMemory);
List list = new ArrayList();
int count = 0;
while (true) {
Log.d("LZQ","[allocate memory] count = " + count);
byte[] test = new byte[20 * 1024 * 1024];
list.add(test);
count++;
}
}
|
4.2.2 测试结果
当前应用未设置largeHeap,故当前设备的应用最大内存为80MB;
往系统申请20MB的内存,仅申请了3*20=60MB的内存,当申请第4块内存时,系统发生OOM,当前的应用内仅剩18MB,不足以继续分配;
4.3 OOM异常定位
OOM异常在log上还是相对明显,有OOM标识:java.lang.OutOfMemoryError。
堆内存分配失败,对应的代码如下(以下流程涉及JVM的内存分配流程,没有进一步展开分析,详细代码可自行阅读):
@art\runtime\gc\heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
...
std::ostringstream oss;
size_t total_bytes_free = GetFreeMemory();
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
<< " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"
<< " target footprint " << target_footprint_.load(std::memory_order_relaxed)
<< ", growth limit "
<< growth_limit_;
...
self->ThrowOutOfMemoryError(oss.str().c_str());
}
|
AndroidStudio工具Profiler,可以查看设备实时内存:
相关用法:https://developer.android.google.cn/studio/profile/memory-profiler?hl=zh-cn
4.4 OOM常见场景
如下Android开发者比较常见的OOM场景:
类型 |
应用场景 |
资源对象没关闭造成的内存泄露 |
Cursor |
调用registerReceiver后未调用unregisterReceiver() |
未关闭InputStream/OutputStream |
Bitmap使用后未调用recycle() |
作用域不一样,导致对象不能被垃圾回收器回收 |
非静态内部类会隐式地持有外部类的引用,handler |
Context泄露:
1、 不要保留对Context-Activity长时间的引用(对Activity的引用的时候,必须确保拥有和Activity一样的生命周期)
2、尝试使用Context-Application来替代Context-Activity
3、如果你不想控制内部类的生命周期,应避免在Activity中使用非静态的内部类,而应该使用静态的内部类,并在其中创建一个对Activity的弱引用。 |
内存压力过大 |
图片资源加载过多,超过内存使用空间,例如Bitmap
的使用 |
重复创建view |
五、LMKD
进程的启动分冷启动和热启动,当用户退出某一个进程的时候,并不会真正的将进程退出,而是将这个进程放到后台,以便下次启动的时候可以马上启动起来,这个过程名为热启动,这也是Android的设计理念之一。这个机制会带来一个问题,每个进程都有自己独立的内存地址空间,随着应用打开数量的增多,系统已使用的内存越来越大,就很有可能导致系统内存不足。
Android 低内存终止守护程序 (Low Memory Killer Daemon) ,可监控运行中的
Android 系统的内存状态,并通过终止最不必要的进程来应对内存压力大的问题,使系统以可接受的性能水平运行。
5.1 LMKD 框架图
5.2 LMKD相关概念
5.2.1 LMKD错误信息
出现低内存异常杀死进程,一般会有lowmemorykiller的tag信息。
08-09 10:33:27.695 376 376 I lowmemorykiller: Kill 'com.android.deskclock' (14849), uid 1000, oom_adj 700 to free 6728kB; reason: low watermark is breached and swap is low (0kB < 108644kB), adjust critical adj.
08-09 10:33:27.695 376 376 D lowmemorykiller: handle notify_lmfs_process_killed done for 14849
08-09 10:33:27.720 383 383 D SurfaceFlinger: Setting power mode 0 on display 0
08-09 10:33:27.807 776 1101 D LmKillerTracker: doLmkForceStop pid=14849
08-09 10:33:27.827 328 328 I Zygote : Process 14849 exited due to signal 9 (Killed)
|
5.2.2 LMKD水位
我们可以观察到低内存出现后,会有如下错误原因: low watermark is breached,即到达低水位,那么Android设备的水位是怎么样的呢?
方式一:
adb shell cat /sys/module/lowmemorykiller/parameters/minfree
adb shell cat /sys/module/lowmemorykiller/parameters/adj
(通过kernel的lmk机制杀死进程,在Android P及以前版本中,采用该方式)
方式二:
adb shell getprop | grep sys.lmk.minfree_levels(在Android
Q及之后,采用该方式)
ps: 绝大多数处理器上的内存页的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者
64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流,ProcessList有定义该值PAGE_SIZE=4KB;
5.2.3 进程优先级
对于每一个运行中的进程,Linux 内核都通过 proc 文件系统暴露 /proc/[pid]/oom_score_adj
这样一个文件来允许其他程序修改指定进程的优先级,这个文件允许的值的范围是:-1000 ~ +1001之间。值越小,表示进程越重要。当内存非常紧张时,系统便会遍历所有进程,以确定哪个进程需要被杀死以回收内存,此时便会读取
oom_score_adj 这个文件的值。
为了便于管理,ProcessList.java中预定义了oom_score_adj的可能取值,这里的预定义值也是对应用进程的一种分类。
@frameworks\base\services\core\java\com\android\server\am\ProcessList.java
static final int INVALID_ADJ = -10000;
static final int UNKNOWN_ADJ = 1001;
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;
static final int CACHED_APP_LMK_FIRST_ADJ = 950;
static final int SERVICE_B_ADJ = 800;
static final int PREVIOUS_APP_ADJ = 700;
static final int HOME_APP_ADJ = 600;
static final int SERVICE_ADJ = 500;
static final int HEAVY_WEIGHT_APP_ADJ = 400;
static final int BACKUP_APP_ADJ = 300;
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;
static final int PERCEPTIBLE_APP_ADJ = 200;
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;
static final int FOREGROUND_APP_ADJ = 0;
static final int PERSISTENT_SERVICE_ADJ = -700;
static final int PERSISTENT_PROC_ADJ = -800;
static final int SYSTEM_ADJ = -900;
static final int NATIVE_ADJ = -1000;
|
如下为测试应用在前台和后台切换,oom_score_adj的变化:
查看myproject应用的进程id:4262
打开应用,查看应用的adj值:0
按home键,应用回到后台,查看应用的adj值:700
打开其他应用,并按home键回到后台,此时查看myproject应用的adj值:800
ps:一般情况下,我们也会将进程分为:前台进程>可见进程>服务进程>后台进程>空进程。
5.2.4 系统内存实时查看
指令:adb shell cat proc/meminfo
5.3 LMKD源码分析
源码分析是一个非常枯燥&无聊的事情,我们需要带着些问题去查阅代码,不然很容易被淹没在代码的海洋里!如下,是我们本次查看源码需要了解的逻辑:
lowmemorykiller的异常信息是在哪打印的?如何杀死进程?
oom_score_adj是怎么样发生变化的?
5.3.1 LMKD流程图
5.3.2 Framework层-AMS服务
5.3.2.1 进程adj变化
1. 进程杀掉后
2. 进程创建后
3. 进程回到后台
5.3.2.2 更新进程adj值
从5.3.1.1节,我们可以看到adj的更新,最终都会引用到setOomAdj().
step 1. 构建buf,写入指令id,进程id,adj值
@frameworks\base\services\core\java\com\android\server\am\ProcessList.java
public static void setOomAdj(int pid, int uid, int amt) {
...
long start = SystemClock.elapsedRealtime();
ByteBuffer buf = ByteBuffer.allocate(4 * 4);
buf.putInt(LMK_PROCPRIO);
buf.putInt(pid);
buf.putInt(uid);
buf.putInt(amt);
writeLmkd(buf, null);
...
}
|
step 2. 打开lmkd的socket端口
private static boolean openLmkdSocketLS() {
try {
sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
sLmkdSocket.connect(
new LocalSocketAddress("lmkd",
LocalSocketAddress.Namespace.RESERVED));
sLmkdOutputStream = sLmkdSocket.getOutputStream();
sLmkdInputStream = sLmkdSocket.getInputStream();
}
...
return true;
}
|
step 3. 往lmkd写入buf数据
private static boolean writeLmkdCommandLS(ByteBuffer buf) {
try {
sLmkdOutputStream.write(buf.array(), 0, buf.position());
} catch (IOException ex) {
Slog.w(TAG, "Error writing to lowmemorykiller socket");
IoUtils.closeQuietly(sLmkdSocket);
sLmkdSocket = null;
return false;
}
return true;
}
|
step 4. 从lmkd读取buf数据
private static boolean readLmkdReplyLS(ByteBuffer buf) {
int len;
try {
len = sLmkdInputStream.read(buf.array(), 0, buf.array().length);
if (len == buf.array().length) {
return true;
}
}
...
}
|
5.3.3 Native层-LMKD进程
5.3.3.1 lmkd进程启动
@system/core/lmkd/lmkd.rc
service lmkd /system/bin/lmkd
class core
user lmkd
group lmkd system readproc
capabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCE NET_ADMIN
critical
socket lmkd seqpacket 0660 system system
socket lmfs stream 0660 root system
socket vmpressure stream 0666 root system
writepid /dev/cpuset/system-background/tasks
|
5.3.3.2 lmkd->main():入口函数
LMKD进程的入口main函数,主要初始化该模块相关参数、消息事件
@\system\core\lmkd\lmkd.c
int main(int argc __unused, char **argv __unused) {
...
level_oomadj[VMPRESS_LEVEL_LOW] =
property_get_int32("ro.lmk.low", OOM_SCORE_ADJ_MAX + 1);
...
if (!init()) {
if (!use_inkernel_interface) {
...
if (mlockall(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) && (errno != EINVAL)) {
ALOGW("mlockall failed %s", strerror(errno));
}
...
}
...
mainloop();
}
...
return 0;
}
|
5.3.3.3 lmkd->init():初始化
step 1. 创建epoll
创建了一个epoll实例,整个lmkd的消息处理都是依赖epoll 机制来管理。其相当于是,创建一个池子,一个监控和管理句柄
fd 的池子,有点像java的线程池;
epollfd = epoll_create(MAX_EPOLL_EVENTS);
if (epollfd == -1) {
ALOGE("epoll_create failed (errno=%d)", errno);
return -1;
}
|
step 2. 初始化socket lmkd
该部分对lmkd端的socket通信进行初始化,其对端AMS.mProcessList会通过/dev/socket/lmkd节点与lmkd进行通信,socket连接成功后,响应事件处理由ctrl_connect_handler函数处理。
ctrl_sock.sock = android_get_control_socket("lmkd");
if (ctrl_sock.sock < 0) {
ALOGE("get lmkd control socket failed");
return -1;
}
ret = listen(ctrl_sock.sock, MAX_DATA_CONN);
if (ret < 0) {
ALOGE("lmkd control socket listen failed (errno=%d)", errno);
return -1;
}
epev.events = EPOLLIN;
ctrl_sock.handler_info.handler = ctrl_connect_handler;
epev.data.ptr = (void *)&(ctrl_sock.handler_info);
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_sock.sock, & epev) == -1) {
ALOGE("epoll_ctl for lmkd control socket failed (errno=%d)", errno);
return -1;
}
|
step 3. 确定是否用LMK 驱动程序
过去,Android 使用内存LMK 驱动程序来监控系统内存的压力,这是一种依赖于硬编码值的硬件机制。从Kernel
4.12开始,LMK驱动程序从上游内核中移除,由应用空间的 lmkd 执行内存监控和进程终止任务。
通过函数access 确认旧的节点是否还存在,用以确认kernel 是否还在用LMK 驱动程序。之所以有这样的处理,应该是Android
为了兼容旧版本kernel。目前Android10上,该节点已不存在。
#define INKERNEL_MINFREE_PATH "/sys/module/lowmemorykiller/parameters/minfree"
has_inkernel_module = !access(INKERNEL_MINFREE_PATH, W_OK);
use_inkernel_interface = has_inkernel_module;
|
step 4. 选择系统内存监控策略
LMKD进程通过使用内核生成的 vmpressure 事件或PSI监视器,获取内存压力等级的通知。但是由于vmpressure信号会存在大量误报的情况,造成不必要的系统开销。因此,Android
10 以及更高版本,使用 PSI 监视器来检测内存压力,且当前Android为了对旧版本的支持,依然保留了vmpressure策略。
PSI (Pressure Stall Information) 压力失速信息,PSI统计数据为即将发生的资源短缺提供了预警功能,因而实现更主动、更细致、更准确的响应。当然PSI统计数据不仅包含了Memory,它同时涵盖了Memory、CPU、IO三大资源的pressure指标,来帮助工程师们及时管控系统资源短缺的情况。
use_psi_monitors = property_get_bool("ro.lmk.use_psi", true) &&
init_psi_monitors();
if (use_psi_vmpressure && use_psi_monitors) {
if (!init_mp_common(VMPRESS_LEVEL_MEDIUM_EXT)) {
ALOGE("Kernel does no support memory pressure events.use psi only.");
}
}
|
本文我们将分析新策略,即PSI策略,来进一步分析lmkd。LMKD是支持新旧策略同时执行的。
step 5. 初始化PSI策略相关行为
确认是使用PSI策略还是vmpressure策略,同时对于不同的策略,初始化相关操作。
static bool init_psi_monitors() {
...
bool use_new_strategy =
property_get_bool("ro.lmk.use_new_strategy", low_ram_device || !use_minfree_levels);
if (use_new_strategy) {
psi_thresholds[VMPRESS_LEVEL_LOW].threshold_ms = 0;
psi_thresholds[VMPRESS_LEVEL_MEDIUM].threshold_ms = psi_partial_stall_ms;
psi_thresholds[VMPRESS_LEVEL_CRITICAL].threshold_ms = psi_complete_stall_ms ;
}
if (!init_mp_psi(VMPRESS_LEVEL_LOW, use_new_strategy)) {
return false;
}
...
return true;
}
|
step 6. PSI内存压力监听&响应
#define PSI_MON_FILE_MEMORY "/proc/pressure/memory"
static bool init_mp_psi(enum vmpressure_level level, bool use_new_strategy) {
...
fd = init_psi_monitor(psi_thresholds[level].stall_type,
psi_thresholds[level].threshold_ms * US_PER_MS,
PSI_WINDOW_SIZE_MS * US_PER_MS);
...
vmpressure_hinfo[level].handler = use_new_strategy ? mp_event_psi_psi : mp_event_common;
vmpressure_hinfo[level].data = level;
if (register_psi_monitor(epollfd, fd, &vmpressure _hinfo[level]) < 0) {
destroy_psi_monitor(fd);
return false;
}
...
return true;
}
|
5.3.3.4 lmkd->mainloop:epoll消息处理
mainloop主要是通过epoll_wait阻塞线程,有消息响应后,再分发消息给对应的handler处理对应逻辑。
static void mainloop(void) {
...
while (1) {
...
if (poll_params.poll_handler) {
...
nevents = epoll_wait(epollfd, events, maxevents, delay);
...
}
...
for (i = 0, evt = &events[0]; i < nevents; ++i, evt++) {
...
if (evt->data.ptr) {
handler_info = (struct event_handler_info*)evt->data.ptr;
handler_info->handler(handler_info->data, evt->events, &poll_params);
...
}
}
}
}
|
epoll主要监听了9个event,不同的fd 对应不同的handler处理逻辑,这些handler大致分为:
一个socket listener fd 监听,主要是/dev/socket/lmkd,在init()
中添加到epoll;
三个客户端socket data fd 的数据通信,在ctrl_connect_handler()
中添加到epoll;
三个presurre 状态的监听,在init_psi_monitors() -> init_mp_psi()
中添加到epoll;(或者init_mp_common 的旧策略)
一个是LMK event kpoll_fd 监听,在init() 中添加到epoll,目前新的lmkd
不再使用这个监听;
一个是wait 进程death 的pid fd 监听,在 start_wait_for_proc_kill()
中添加到epoll;
5.3.3.5 lmkd->ctrl_command_handler():处理AMS下发事件
AMS下发事件主要有如下,其他的事件处理雷同:
更新OomLevels水位,将minfree和oom_adj_score进行保存&组装,然后将组装的字符串存入到prop
sys.lmk.minfree_levels。后续会根据minfree和oom_adj_score,来决定进程的查杀。
更新oom_adj_score,将AMS 中传下来的进程的oom_score_adj 写入到节点 /proc/pid/oom_score_adj;
static void ctrl_command_handler(int dsock_idx) {
...
switch(cmd) {
case LMK_TARGET:
...
cmd_target(targets, packet);
break;
case LMK_PROCPRIO:
...
cmd_procprio(packet);
break;
case LMK_PROCREMOVE:
...
cmd_procremove(packet);
break;
case LMK_PROCPURGE:
...
cmd_procpurge();
break;
...
}
...
}
static void cmd_target(int ntargets, LMKD_CTRL_PACKET packet) {
...
for (i = 0; i < ntargets; i++) {
lmkd_pack_get_target(packet, i, &target);
lowmem_minfree[i] = target.minfree;
lowmem_adj[i] = target.oom_adj_score;
pstr += snprintf(pstr, pend - pstr, "%d:%d,", target.minfree,
target.oom_adj_score);
...
}
pstr[-1] = '\0';
property_set("sys.lmk.minfree_levels", minfree_str);
...
}
static void cmd_procprio(LMKD_CTRL_PACKET packet) {
...
snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", params.pid);
snprintf(val, sizeof(val), "%d", params.oomadj);
if (!writefilestring(path, val, false)) {
ALOGW("Failed to open %s; errno=%d: process %d might have been killed",
path, errno, params.pid);
return;
}
...
}
|
5.3.3.6 lmkd->mp_event_psi():进程查杀
step 1. 解析/proc/vmstat和/proc/meminfo节点
if (vmstat_parse(&vs) < 0) {
ALOGE("Failed to parse vmstat!");
return;
}
if (meminfo_parse(&mi) < 0) {
ALOGE("Failed to parse meminfo!");
return;
}
|
step 2. 根据vmstat节点的状态,计算工作集refault值占据file-backed页面缓存的抖动百分比。
vmstat(Virtual Memory Statistics),用于报告虚拟内存状态的统计信息,不仅可以监测虚拟内存,也可监测进程、物理内存、内存分页、磁盘和
CPU 等的活动信,是对系统的整体情况进行统计
if (!in_reclaim) {
base_file_lru = vs.field.nr_inactive_file + vs.field.nr_active_file;
init_ws_refault = vs.field.workingset_refault;
thrashing_limit = thrashing_limit_pct;
} else {
thrashing = (vs.field.workingset_refault - init_ws_refault) * 100 / base_file_lru;
}
in_reclaim = true;
|
step 3. 间隔60s,解析/proc/zoneinfo,并计算min/low/hight水位线
if (watermarks.high_wmark == 0 || get_time_diff_ms(&wmark_update_tm, &curr_tm) > 60000) {
struct zoneinfo zi;
if (zoneinfo_parse(&zi) < 0) {
ALOGE("Failed to parse zoneinfo!");
return;
}
calc_zone_watermarks(&zi, &watermarks);
wmark_update_tm = curr_tm;
}
|
step 4. 根据mi,判断当前所处的水位线
enum zone_watermark {
WMARK_MIN = 0,
WMARK_LOW,
WMARK_HIGH,
WMARK_NONE
};
wmark = get_lowest_watermark(&mi, &watermarks);
|
step 5. 根据水位线、thrashing值、压力值、swap_low值等数据,添加不同的kill原因
if (cycle_after_kill && wmark < WMARK_LOW) {
kill_reason = PRESSURE_AFTER_KILL;
strncpy(kill_desc, "min watermark is breached even after kill", sizeof(kill_desc));
} else if (level == VMPRESS_LEVEL_CRITICAL && events != 0) {
kill_reason = NOT_RESPONDING;
do_multi_kill = low_ram_device ? true : false;
strncpy(kill_desc, "device is not responding", sizeof(kill_desc));
} else if (fast_kill_enabled) {
kill_reason = DO_FAST_KILL;
strncpy(kill_desc, "do fast kill", sizeof(kill_desc));
min_score_adj = low_ram_device ? CACHED_APP_MIN_ADJ : PERCEPTIBLE_LOW_APP_ADJ;
min_score_adj = swap_is_low ? PERCEPTIBLE_LOW_APP_ADJ : min_score_adj;
} else if (swap_is_low && thrashing > thrashing_limit_pct) {
kill_reason = LOW_SWAP_AND_THRASHING;
snprintf(kill_desc, sizeof(kill_desc), "device is low on swap (%" PRId64
"kB < %" PRId64 "kB) and thrashing (%" PRId64 "%%)",
mi.field.free_swap * page_k, swap_low_threshold * page_k, thrashing);
} else if (swap_is_low && wmark < WMARK_HIGH) {
kill_reason = LOW_MEM_AND_SWAP;
snprintf(kill_desc, sizeof(kill_desc), "%s watermark is breached and swap is low (%"
PRId64 "kB < %" PRId64 "kB)", wmark > WMARK_LOW ? "min" : "low",
mi.field.free_swap * page_k, swap_low_threshold * page_k);
} else if (wmark < WMARK_HIGH && thrashing > thrashing_limit) {
kill_reason = LOW_MEM_AND_THRASHING;
snprintf(kill_desc, sizeof(kill_desc), "%s watermark is breached and thrashing (%"
PRId64 "%%)", wmark > WMARK_LOW ? "min" : "low", thrashing);
cut_thrashing_limit = true;
min_score_adj = PERCEPTIBLE_APP_ADJ;
} else if (reclaim == DIRECT_RECLAIM && thrashing > thrashing_limit) {
kill_reason = DIRECT_RECL_AND_THRASHING;
snprintf(kill_desc, sizeof(kill_desc), "device is in direct reclaim and thrashing (%"
PRId64 "%%)", thrashing);
cut_thrashing_limit = true;
min_score_adj = PERCEPTIBLE_APP_ADJ;
}
|
step 6. 根据当前zone的信息,以及通过lowmem_adj和lowmem_minfree水位线,重新生成min_score_adj,用于决定需要杀死的进程等级
if (other_free <= minfree && other_file <= minfree) {
min_score_adj_adjust = lowmem_adj[0];
strcat(kill_desc, ", adjust adj0.");
} else if (other_free <= lowmem_minfree[1] && other_file <= lowmem_minfree[1]) {
min_score_adj_adjust = lowmem_adj[1];
strcat(kill_desc, ", adjust adj1.");
}
if (min_score_adj_adjust == -1 && !min_score_adj) {
min_score_adj = VISIBLE_APP_ADJ;
strcat(kill_desc, ", adjust critical adj.");
} else if (min_score_adj_adjust != -1) {
min_score_adj = min_score_adj_adjust;
}
|
step 7. 根据min_score_adj水位线,查找并杀死对应的进程
if (do_multi_kill) {
do_multi_kill = false;
if (NOT_RESPONDING == kill_reason) {
min_score_adj = swap_is_low ? 0 : min_score_adj;
strcat(kill_desc, " <kill all processes above>");
pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
} else if (swap_is_low) {
strcat(kill_desc, " <kill all processes above>");
pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
} else {
strcat(kill_desc, " <kill group processes>");
pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, false);
}
} else if (fast_kill_enabled) {
fast_kill_enabled = false;
pages_freed = find_and_kill_multi_processes(min_score_adj, kill_desc, true);
} else
pages_freed = find_and_kill_process(min_score_adj, kill_desc);
|
step 8. lmkd白名单,避免被杀死
调用栈:find_and_kill_process()->proc_adj_lru_skip()->adjslot_skip()->lmkd_skip_kill()->lmkd_config_skip_kill()
@system\core\lmkd\lmkconfig.c
#define LMKD_PARAMETER_NAME "/vendor/etc/lmkd_param.conf"
bool lmkd_config_skip_kill(char *task_name)
{
PARAM_INFO param_info;
int count = 0;
int number = 0;
memset(¶m_info, 0, sizeof(param_info));
if (get_param_info(¶m_info, CONFIG_LMKD_SKIP_PROCESS_LIST) == true) {
count = param_info.proc_count;
while(count) {
number = count - 1;
if (!memcmp(param_info.proc_info[number].task_info, task_name, strlen(param_info.proc_info[number].task_info) -1))
return true;
count --;
}
}
return false;
}
|
5.4 LMKD小结
在kernel4.12之前,采用的是linux内核的lmk机制查杀进程;
在kernel4.12之后,Android 9采用用户空间lmkd的vmpressure策略,来查杀进程;
Android 10之后采用lmkd的Psi策略,查杀进程;
framework层与Lmkd是通过socket实现ipc通信;
lmkd的socket通信通过epoll机制管理;
lmkd可以通过配置白名单,避免被查杀;
5.5 遗留问题
从业务代码上看,PSI策略并没有完全遵循sys.lmk.minfree_levels水位的查杀,vmpressure策略相对来说更加遵循一点,有点意外,需要再确认此问题?
内存管理实际是基于如下节点:/proc/vmstat、/proc/meminfo和/proc/zoneinfo,需要了解其含义以及由来?
/proc/pressure/memory内存压力的实现逻辑?
|