Android内存回收机制——Activity被系统回收了?
日常开发中经常会出现这样的情景,测试跟开发唇枪舌战“为什么从其他app返回,页面内容要重新加载呢,这肯定是个bug”,“Activity被系统回收了,不是bug”;“为什么切换到后台,这个app心跳不在线了呢,这肯定也是bug”,“这是后台进程被系统回收了,算什么bug”。内存回收是在Android开发中经常接触到的概念,但是你真的了解内存回收吗?这篇我们就来简单探讨下Android的内存回收机制。
Android内存回收机制
Addroid系统在设计时处于用户体验和性能优化的角度,设计了LMK机制:Low Memory Killer
简称LMK,用于处理内存回收调度。
简单来说就是,当app切换到后台时,为了能在用户再次打开app时,及时进行响应加快app打开时间,Android系统对于切换到后台的app,并不会立即杀死回收,而是在保证系统资源足够的情况下,尽可能的提升app进程在系统内存中的留存时间,只有当系统内存资源不足时,按照一定的优先级将app进程回收,这套机制就是LMK。
Android进程分类
Android系统根据app当前状态和组件运行声明周期,按照进程重要程度,将app进程进行了分类。
在Android 5.0之前,系统将app进程分为了5类,分别是:
-
前台进程——
IMPORTANCE_FOREGROUND
- 简单来说:当前用户正在操作的进程。
- 比如一下任意一个,都会被认为是前台进程:
- 它正在用户的互动屏幕上运行一个
Activity
(其onResume()
方法已被调用)。 - 它有一个
BroadcastReceiver
目前正在运行(其BroadcastReceiver.onReceive()
方法正在执行)。 - 它有一个
Service
目前正在执行其某个回调(Service.onCreate()
、Service.onStart()
或Service.onDestroy()
)中的代码。
- 它正在用户的互动屏幕上运行一个
-
可见进程——
IMPORTANCE_VISIBLE
- 正在进行用户当前知晓的任务,也就是说能够被用户感知到的进程。
- 比如:
- 它正在运行的
Activity
在屏幕上对用户可见,但不在前台(其onPause()
方法已被调用)。举例来说,如果前台 Activity 显示为一个对话框,而这个对话框允许在其后面看到上一个 Activity,则可能会出现这种情况。 - 它有一个
Service
正在通过Service.startForeground()
(要求系统将该服务视为用户知晓或基本上对用户可见的服务)作为前台服务运行。 - 系统正在使用其托管的服务实现用户知晓的特定功能,例如动态壁纸、输入法服务等。
- 它正在运行的
-
服务进程——
IMPORTANCE_SERVICE
- 含一个已使用
startService()
方法启动的Service
。 -
注意:
- 这里说的服务进程是指Service是单独的进程,此进程中没有启动过Activity。
- 并且在30分钟内活跃过的服务进程。
- 含一个已使用
-
后台进程——
IMPORTANCE_BACKGROUND
- app切换到后台,app通常包含用户当前不可见的一个或多个
Activity
实例(onStop()
方法已被调用并返回)。
- app切换到后台,app通常包含用户当前不可见的一个或多个
- 空白进程——
IMPORTANCE_EMPTY
当然在5.0之后的Android系统中,对进程分类再次进行的细化,比如:
- Android 5.0,新增了
-
IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
-
IMPORTANCE_CACHED
- 等同于
IMPORTANCE_BACKGROUND
- 等同于
- Android 11.0,新增到了9种
-
IMPORTANCE_TOP_SLEEPING
- 前台app,但是系统进入的休眠,此app进程。
-
IMPORTANCE_FOREGROUND_SERVICE
- 将
Service.startForeground()
前台服务这种情况进行了拆分,单列。
- 将
-
-
这里就不详细介绍了,可以通过源码:android.app.ActivityManager.RunningAppProcessInfo
查看详细的定义和注释介绍。
ADJ
刚开始提到了当系统内存资源不足时,系统会按照一定的优先级将app进程回收,这个优先级就是adj。
App进程启动时,Android系统会为其分配一个单独的adj,adj的取值会随着进程的状态和其组件的生命周期动态变化。
查看系统adj
Android维护了一套adj值与可用内存的对应关系,他们是一一对应的,当可用内存达到阈值时,会将对应adj的进行进行回收,释放资源。
minfree和adj文件是描述系统可用内存与对应进程优先级的关系,分成多个等级两个文件值是一一对应的。
aosp:/sys/module/lowmemorykiller/parameters # cat minfree
18432,23040,27648,32256,36864,46080
aosp:/sys/module/lowmemorykiller/parameters # cat adj
0,100,200,300,900,906
通过查看sys/module/lowmemorykiller/parameters/
下的minfree
和adj
文件,可用获得内存阈值与adj对应关系。以上面数据为例:
注意:menfree值单位是page, 1page = 4K。
当系统可用内存小于46080 * 4 / 1024 = 180(M)
时,系统会将adj大于906的进程回收,从而释放内存,同理,当系统可用内存小于18432 * 4 / 1024 = 72 M
时,系统会将adj大于0的进程回收。
应用进程adj
App进程启动时,Android系统会为其分配一个单独的adj,进程adj的大小也可以通过adb命令进行查看。
// 查看应用进程号
aosp:/ # ps |grep com.zhong.event
u0_a45 2104 1060 1153372 92444 0 c7f29c02 S com.zhong.event
// 查看进程adj值
aosp:/ # cat /proc/2104/oom_score_adj
// com.zhong.event为前台应用,adb = 0
0
进程adj值,可以通过查看文件/proc/进程号/oom_score_adj
获得。
我们按Home键,将应用切换到后台,再次查看其adj值:
aosp:/ # cat /proc/2104/oom_score_adj
700
手动kill掉进程后再次查看:
aosp:/ # cat /proc/2104/oom_score_adj
sh: cat: /proc/2104/oom_score_adj: No such file or directory
1|aosp:/ #
可以发现,进程的adj值是动态变化的,且只有当应用进程存活时,系统才会为期分配adj。
注意:adj取值根据系统版本不同,硬件配置不同,会有差异,但是关系是一样的。
adj级别
adj值是动态变化的,系统会根据当前app状态及四大组件生命周期变化等因素,动态改变adj值,常见级别如下:
以下类型的adj取值,同样会因为不同Android版本不同,硬件配置不同,甚至不同厂家等原因会有所差异。
ADJ级别 | 取值 | 含义 |
---|---|---|
NATIVE_ADJ | -1000 | native进程 |
SYSTEM_ADJ | -900 | 仅指system_server进程 |
PERSISTENT_PROC_ADJ | -800 | 系统persistent进程 |
PERSISTENT_SERVICE_ADJ | -700 | 关联着系统或persistent进程 |
FOREGROUND_APP_ADJ | 0 | 前台进程,用户目前执行操作所需的进程。 |
VISIBLE_APP_ADJ | 100 | 可见进程,正在进行用户当前知晓的任务, |
PERCEPTIBLE_APP_ADJ | 200 | 可感知进程,ForegroundService |
BACKUP_APP_ADJ | 300 | 备份进程 |
HEAVY_WEIGHT_APP_ADJ | 400 | 重量级进程 |
SERVICE_ADJ | 500 | 服务进程,纯Service进程 |
HOME_APP_ADJ | 600 | Home进程,launcher app |
PREVIOUS_APP_ADJ | 700 | 上一个进程 |
SERVICE_B_ADJ | 800 | B List中的Service,资源不足时Service进程,降级 |
CACHED_APP_MIN_ADJ | 900 | 不可见进程的adj最小值,activity finish |
CACHED_APP_MAX_ADJ | 906 | 不可见进程的adj最大值 |
我们重点关注下表格中加粗部分就可以了,可以看到这其中好多类型与我们上文介绍的Android进程分类有很明显的对应关系。
adj < 0
这些进程基本都是系统处理的进程,不用太多关注。唯一需要注意的是PERSISTENT_SERVICE_ADJ
这种情况,当app的AndroidManifest.xml中添加了android:persistent="true"
属性,并且app必须是系统内置app(必须内置/system/app下),满足这个必要条件的基础上,系统在开机时会自动创建app进程,并且进程的优先级是PERSISTENT_SERVICE_ADJ(-800)
。当手动杀死进程时,系统还会自动重启进程,这在开发系统内置应用时,处理进程保活会很有效。
FOREGROUND_APP_ADJ, adj = 0
对应进程类型的前台进程,当app满足一下任意条件时,就属于adj = 0的情况:
- 它正在用户的互动屏幕上运行一个
Activity
(其onResume()
方法已被调用)。 - 它有一个
BroadcastReceiver
目前正在运行(其BroadcastReceiver.onReceive()
方法正在执行)。 - 它有一个
Service
目前正在执行其某个回调(Service.onCreate()
、Service.onStart()
或Service.onDestroy()
)中的代码。
VISIBLE_APP_ADJ, adj = 100
对应可见进程,但是不包含Service.startForeground()
前台服务情况:
- 它正在运行的
Activity
在屏幕上对用户可见,但不在前台(其onPause()
方法已被调用)。举例来说,如果前台 Activity 显示为一个对话框,而这个对话框允许在其后面看到上一个 Activity,则可能会出现这种情况。
PERCEPTIBLE_APP_ADJ, adj = 200
使用Service.startForeground()
启动前台服务进程。
SERVICE_ADJ, adj = 500
对应服务进程:
- 这里说的服务进程是指Service是单独的进程,此进程中没有启动过Activity。
- 并且在30分钟内活跃过的服务进程。
HOME_APP_ADJ, adj = 600
Home进程,对应launcher属性的进程,类型为ACTIVITY_TYPE_HOME的应用。
PREVIOUS_APP_ADJ, adj = 700
用户使用的上一个进程(有activity没有finish),通常是用户两个app间切换时,被切换到后台的应用。
这里“上一个”的概念,不单只上一个使用app,如:用户从A应用切换到B应用,又从B应用切换到C应用,这时A应用可能是PREVIOUS_APP_ADJ
adj = 700。
CACHED_APP_MIN_ADJ, adj = 900
对应后台进程,app切换到后台:
- app通常包含用户当前不可见的一个或多个
Activity
实例(onStop()
方法已被调用并返回)。 - 通常是所有Activity都被finish。
其他的adj类型,大都是一些进程的特殊状态,不再详述了,这里在提一下SERVICE_B_ADJ
。
SERVICE_B_ADJ, adj = 800
SERVICE_B_ADJ
类型,可以理解成是SERVICE_ADJ
状态的降级,当出现一些特殊情况,如资源不足Service进程可能会被降级成SERVICE_B_ADJ
。
除此之外,对应同级别,同样adj值的进程,系统会优先回收占用内存高的app进程。
引用部分官方文档内容:
低内存终止守护进程
很多时候,
kswapd
不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作。LMK 使用一个名为 oom_adj_score 的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止。下表列出了从高到低的 LMK 评分类别。评分最高的类别,即第一行中的项目将最先被终止:
以下是上表中各种类别的说明:
- 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高
oom_adj_score
的应用开始终止后台应用。- 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。
- 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。
- 服务:服务由应用启动,可能包括同步或上传到云端。
- 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。
- 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。
- 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。
- 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。
- 原生:系统使用的极低级别的进程(例如,
kswapd
)。设备制造商可以更改 LMK 的行为。
更多关于ADJ详细的介绍和相关介绍,可以参考大佬的文章:《解读Android进程优先级ADJ算法》,讲解的特别详细了,膜拜大佬。。
从进程回收来看服务保活
了解了Android进程回收的机制和原理,对后台服务的保活也有一定的启发,简单来说就是尽可能的提高服务进程的adj级别:
- 特殊情况下可以通过
android:persistent="true"
属性将进程提升至PERSISTENT_SERVICE_ADJ(-800)
级别,近乎无敌。当然了条件也是非常苛刻的:app必须内置为系统应用。 - 使用前台服务
Service.startForeground()
,将进程提升至PERCEPTIBLE_APP_ADJ(200)
。 - UI进程与服务进程分离,当app切换到后台,未分离的Service进程(app进程)降级为
CACHED_APP_MIN_ADJ(900)
,而进程分离的Service进程,始终保持为SERVICE_ADJ(500)
。 - 内存优化,对应同级别,同样adj值的进程,系统会优先回收占用内存高的app进程。
小结
回到最开始的问题:Activity被系统回收了?只可能是因为整个app进程被系统回收吗?其实并不是,我们知道Android系统为每个app进程分配一个内存上限,当app使用内存,超过此上限时,就会发生OOM异常。针对此场景Android系统也制定了一些对Activity栈进行回收的机制,简单来说:
对于单栈(TaskRecord)应用,在前台的时候,所有界面都不会被回收,只有多栈情况下,系统才会回收不可见栈的Activity。
具体可以参考Android可见APP的不可见任务栈(TaskRecord)销毁分析。
参考文章:
- 解读Android进程优先级ADJ算法
- App处于前台,Activity就不会被回收了?
- Android可见APP的不可见任务栈(TaskRecord)销毁分析
- Android内存回收机制
- Android Developers 进程和应用生命周期
- Android Developers 进程间的内存分配
推荐阅读
-
Android内存回收机制——Activity被系统回收了?
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。