前言
本章主要围绕内存相关的知识点讲解;
内存分配
在内存优化中,我们通常需要借助一些常用的 adb 命令,方便我们快速定位,下面是一些常用的 adb 命令总结
常用 adb 命令
adb shell getprop ro.product.model // 手机型号adb shell dumpsys battery //电池信息adb shell wm size //屏幕尺寸adb shell wm density //屏幕像素密度adb shell dumpsys window displays // 查看总得设备信息,包含了显示屏编号,像素密度,分辨率等等adb shell settings get secure android_id // android id 一般用来标识不同的手机adb shell service call iphonesubinfo 1 // IMEI adb shell getprop ro.build.version.release // 版本adb shell cat /proc/cpuinfo // cpu 信息adb shell cat /system/build.prop // 查看构建文件信息adb shell cat /proc/meminfo // 查看内存信息adb shell dumpsys meminfo //获取内存信息adb shell procrank //查看进程内存排名adb shell topadb shell top |grep your app nameadb shell vmstat // adb shell vmstat 2 // adb shell top -d 20 > meminfo
内存指标概念
RSS(共享库、so 动态链接库)
USS 进程独占内存空间
procs(进程)
r: Running队列中进程数量
b: IO wait的进程数量
memory(内存)
free: 可用内存大小
mapped:mmap映射的内存大小
anon: 匿名内存大小
slab: slab的内存大小
system(系统)
in: 每秒的中断次数(包括时钟中断)
cs: 每秒上下文切换的次数
cpu(处理器)
us: user time (用户时间)
ni: nice time
sy: system time(系统时间)
id: idle time
wa: iowait time
ir: interrupt time
用户时间 + 系统时间 = 进程时间;
内存分配
Java 内存分配模型
内存分配模型,详细的讲解可以看我之前的文章(https://juejin.cn/post/7314365534107238438)
java 对象生命周期
- 创建
- 为对象分配内存空间,调用构造方法构造对象
- 应用
- 此时对象至少被一个强引用持有
- 不可见
- 对象还存在,但是没有被强引用了,如果被GC扫描到了,就会进行可达性分析
- 不可达
- 可达性分析,发现不可达(也就是没有任何强应用了)
- 收集
- GC准备对该对象内存空间进行重新分配
- 如果重写了finalize方法,这个方法就会被调用
- 终结
- 被垃圾回收器 回收
- 对象空间重新分配
- 对象被回收之后,这块空间重新分配
java 对象的内存布局
- 对象头
- 存储对象自身的运行时数据
- 哈希码
- GC 分代年龄
- 锁状态标识
- 线程持有的锁
- 偏向线程 ID
- 偏向时间戳
- 类型指针
- 若为对象数据,还应该记录数组长度的数据
- 存储对象自身的运行时数据
- 实例数据
- 对齐填充
本地方法栈
线程私有,每个线程不一样
程序计数器
线程切换使用,也是线程私有;
垃圾回收算法
标记清除
位置不连续,产生内存碎片,效率略低,两遍扫描。多次进行标记清除算法,内存就会千疮百孔;先标记存活对象,然后清除回收对象,产生内存碎片;
复制算法
实现简单,运行高效,没有内存碎片,但是利用率只有一半;将内存一分为二,将存活对象复制到剩余的一半,其余的回收
标记整理算法
没有内存碎片,效率偏低,两边扫描、指针需要调整;先标记,然后将存活对象整理到一起。剩下的回收掉
分代收集算法
综合应用上面的算法;
四种引用
Android 内存回收机制
Android Kill 机制
- 在 Android 的 lowmemroykiller 机制中,会对于所有进程进行分类,对于每一类别的进程会有其 oom_adj 值的取值范围,oom_adj值越高则代表进程越不重要,在系统执行低杀操作时,会从 oom_adj 值越高的开始杀;
- 对于期望较长时间留在后台的服务,应该将服务运行在单独的进程里,即是 UI 进程与 Servie 进程分离,这样期望长时间留在后台的 Serivce 会存在与一个被 lowmemorykiller 分类为 Service 进程的服务而获得较小的Adj 值,而占有大量内存的UI进程则会分类为 Cached 进程,能够在需要的时候更快地被回收;
oom_adj
- 当所有应用退后台之后并不会立马被杀掉,而是通过 oom_adj 进行了一个分级[-16, 15] 从 -16 到 15 的这样的一个区间,应用对应的数字越小 越不容易被 OOM Killer 杀到
- ams 又对齐进行了一个更高等级的区分 oom_score_adj,将应用优先级的区间划分为[-1000, 1000],从 -1000 到 1000,score 的值越低越不容易被杀掉;
- 如果两个应用的 oom_adj 值一样,那么哪个 app 占用内存多,哪个就优先被杀掉。所以要尽可能的降低应用进入后台后的内存,才能保证尽可能的不被系统杀掉;
Android App内存组成以及限制
Android 给每一个 App 都分配一个 VM,让 App 运行在 Dalvik 上,这样即使 App 崩溃了也不会影响到系统,系统给 VM 分配了一定的内存大小,App 可以申请使用的内存大小不会超过此硬性逻辑限制,就算物理内存富余,如果应用超出 VM 最大内存,就会出现内存溢出 crash;
由程序控制操作的内存空间在 heap 上,分为 java heapsize 和 native heapsize;
native 层内存申请不受其限制,native 层受 native process 对内存大小的限制;
修改系统为每个App分配的内存大小;默认为每个app分配 16m 的内存大小;
LeakCanary 源码解析
从垃圾回收机制、源码分析、优缺点中分析;
总结的详细流程如下:
- 通过 provider 进行 Leakcanary 的初始化逻辑
- provider 的 onCreate 方法中,获取 Application
- 加载 Leakcanary
- 检查当前线程是否在主线程
- 检查是否已经加载了 Leakcanary,如果加载了,则直接返回
- 创建 Activity 的监视
- 创建 Activity 的 onDestory 回调监视
- 将可达的 Activity 删除
- 根据 Activity 创建对应的弱引用,并绑定 ReferenceQueue
- 将 reference 保存到 watchedObjects 数组中
- 启动延时 5s 任务
- 获取 GC 无法回收的 Activity
- 通知内存泄露
- 同 Application 绑定
- 创建 Activity 的 onDestory 回调监视
- 创建 Fragment 的监视
- 调用上层模块 InternalLeakCanary.invoke
本质就是:监听 Activity 的 onDestory 方法回调,过 5s 之后进行一次 GC,通过 WeakReference 引用链看它有没有销毁;
LK 中使用的 垃圾回收算法
可达性分析算法、引用计算算法;
可以作为 GC Root 的对象有哪些?
- 在线程栈中的局部变量(即正在被调用的方法里面的参数和局部变量)
- 存活的线程对象
- JNI的引用
- Class对象(在Android中Class被加载后是不会被卸载的)
- 引用类型的静态变量
模块层级(LK都有哪些模块,以及对应的模块都是做什么的?)
leakcanary-android
集成入口模块,提供 LeakCanary 安装,公开 API 等能力
leakcanary-android-core
核心模块
lealcanary-object-watcher、lealcanary-object-watcher-android、lealcanary-object-watcher-android-androidx、lealcanary-object-watcher-android-support-fragment
对象实例观察模块,在 Activity,Fragment 等对象的生命周期中,注册对指定对象实例的观察,有 Activity,Fragment,Fragment View,ViewModel 等;
leakcanary-android-process
和 leakcanary-android 一样,区别是会在单独的进程进行分析;
shark
hprof 文件解析与分析的入口模块;
shark-android
提供特定于 Android 平台的分析能力。例如设备的信息,Android 版本,已知的内存泄露问题等;
shark-graph
分析堆中对象的关系图模块;
shark-hprof
解析 hprof 文件模块;
share-log
日志模块;
LeakCanary 注册
<application><providerandroid:name="leakcanary.internal.AppWatcherInstaller$MainProcess"android:authorities="${applicationId}.leakcanary-installer"android:exported="false"/></application>
这是通过注册 provider 进行 Leakcanary 的初始化逻辑,我们进入 AppWatcherInstaller 中看下
internal sealed class AppWatcherInstaller : ContentProvider() {internal class LeakCanaryProcess : AppWatcherInstaller() {override fun onCreate(): Boolean {super.onCreate()AppWatcher.config = AppWatcher.config.copy(enabled = false)return true}}override fun onCreate(): Boolean {// 获取 Applicationval application = context!!.applicationContext as Application// 加载 LeakCanaryInternalAppWatcher.install(application)return true}
}
加载 Leakcanary,我们进入这个方法看下:
internal object InternalAppWatcher {// 加载fun install(application: Application) {// 检查当前线程是否在主线程checkMainThread()if (this::application.isInitialized) {// 如果 Leakcanary 已经加载过了,直接返回return}InternalAppWatcher.application = applicationval configProvider = { AppWatcher.config }// 监视 ActivityActivityDestroyWatcher.install(application, objectWatcher, configProvider)// 监视 FragmentFragmentDestroyWatcher.install(application, objectWatcher, configProvider)// 调用上层模块InternalLeakCanary.invokeonAppWatcherInstalled(application)}
}
我们进入 ActivityDestroyWatcher 看下 Activity 是如何被监视的;
internal class ActivityDestroyWatcher private constructor(private val objectWatcher: ObjectWatcher,private val configProvider: () -> Config
) {// 创建 Activity 的 destory 监听回调private val lifecycleCallbacks =object : Application.ActivityLifecycleCallbacks by noOpDelegate() {override fun onActivityDestroyed(activity: Activity) {// Activity 的 onDestory 回调,触发存在对象检查if (configProvider().watchActivities) {// 通过 objectWatcher 监视 ActivityobjectWatcher.watch(activity)}}}companion object {fun install(application: Application,objectWatcher: ObjectWatcher,configProvider: () -> Config) {// 创建 Activity 的 onDestory 监听回调val activityDestroyWatcher =ActivityDestroyWatcher(objectWatcher, configProvider)// 绑定 Application application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)}}
}
我们进入 ObjectWatcher 中看下 Acivity 是如何被监视的;
@Synchronized fun watch(watchedObject: Any,name: String) {if (!isEnabled()) {return}// 将可达的 activity 删除removeWeaklyReachableObjects()val key = UUID.randomUUID().toString()val watchUptimeMillis = clock.uptimeMillis()// 根据 activity 创建对应的弱引用,并绑定ReferenceQueueval reference =KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)// 将 reference 保存到 watchedObjects 数组中watchedObjects[key] = reference// 启动延时 5s 任务checkRetainedExecutor.execute {// 获取 GC 无法回收的 ActivitymoveToRetained(key)}}
我们进入这个 removeWeaklyReachableObjects 方法看下,可达的 Activity 是怎么删除的;
private fun removeWeaklyReachableObjects() {var ref: KeyedWeakReference?do {// 重点,在 GC 或者 finalization 之前,在 WeakReferences 的被引用对象(这里是Activity)的可达性更改时,会把 WeakReferences 添加到创建时候指定的 ReferenceQueue 队列,这些可达性变更得对象,就是内存不泄露对象ref = queue.poll() as KeyedWeakReference?if (ref != null) {// 在 watchedObjects 中删除不发送内存泄漏对象,剩下内存泄漏对象;watchedObjects.remove(ref.key)}} while (ref != null)}
我们接着看下这个延迟 5s 任务是如何创建的;
val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)private val checkRetainedExecutor = Executor {//在主线程延时五秒执行任务mainHandler.postDelayed(it, AppWatcher.config.watchDurationMillis)
}
我们接着看下无法回收的 Activity 是如何获取的,进入 moveToRetained 方法看下;
@Synchronized private fun moveToRetained(key: String) {// 将可达activity删除removeWeaklyReachableObjects()val retainedRef = watchedObjects[key]if (retainedRef != null) {// 保存当前时间作为泄漏时间retainedRef.retainedUptimeMillis = clock.uptimeMillis()// 通知 InternalLeakCanary 发生内存泄漏onObjectRetainedListeners.forEach { it.onObjectRetained() }}
}
我们来看下内存泄露的场景是如何进行上报的,我们进入 HeapDumpTrigger 的 onObjectRetained 方法看下;
fun onObjectRetained() {// 再次检查内存是否泄露scheduleRetainedObjectCheck("found new object retained")
}
我们进入 scheduleRetainedObjectCheck 方法看下;
private fun scheduleRetainedObjectCheck(reason: String,delayMillis: Long) {if (checkScheduled) {return}checkScheduled = truebackgroundHandler.postDelayed({checkScheduled = false// 检查泄漏对象checkRetainedObjects(reason)}, delayMillis)}
我们进入 checkRetainedObjects 方法看下;
private fun checkRetainedObjects(reason: String) {...if (retainedReferenceCount > 0) {// 执行一次 GC,确认所有存在对象都是泄露对象;gcTrigger.runGc()retainedReferenceCount = objectWatcher.retainedObjectCount}// 检查当前所有存在对象的个数if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) returnif (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {showRetainedCountWithDebuggerAttached(retainedReferenceCount)// 如果配置了 debug 不使用 heap 且正在 debug,延时 20s 在执行checkRetainedObjects(reason)scheduleRetainedObjectCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)return}val heapDumpUptimeMillis = SystemClock.uptimeMillis()KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillisdismissRetainedCountNotification()// 执行dump Heap操作val heapDumpFile = heapDumper.dumpHeap()lastDisplayedRetainedObjectCount = 0objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)HeapAnalyzerService.runAnalysis(application, heapDumpFile)}
我们来看下当前所有存在对象的个数是如何检查的;
private fun checkRetainedCount(retainedKeysCount: Int,retainedVisibleThreshold: Int): Boolean {val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCountlastDisplayedRetainedObjectCount = retainedKeysCountif (retainedKeysCount == 0) {// 存在对象为 0return true}if (retainedKeysCount < retainedVisibleThreshold) {// 存在对象低于阈值5个if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)// 当前应用可见,或者不可见时间间隔少于 5s,重新安排到 2s 后执行 checkRetainedObjectsscheduleRetainedObjectCheck("Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS)return true}}return false}
到此,Leakcanary 的流程就整体跑完了;
线上内存如何监控
在引入任何自动分析工具之前,对于 Activity 泄漏,一般都是在自动化测试阶段监控内存占用,一旦超过预期,则发起一次 GC 后进行 Dump Hprof 操作。分析人员将 Hprof 文件导入 MAT 中查看各个 Activity 的引用链,找出被静态成员或 Application 对象等长生命周期对象持有的 Activity,再进行修复。对于冗余的 Bitmap,也是将 Hprof 导入 Android Monitor 后通过 Android Monitor 自带的工具找出冗余的 Bitmap 对象。
自动化监测目标流程
- 自动且较为准确地监测 Activity 泄漏,发现泄漏之后再触发 Dump Hprof 而不是根据预先设定的内存占用阈值盲目触发;
- 自动获取泄漏的 Activity 和冗余 Bitmap 对象的引用链;
- 能灵活地扩展 Hprof 的分析逻辑,必要时允许提取 Hprof 文件人工分析;
监测阶段
Activity 对象被生命周期更长的对象通过强引用持有,使 Activity 生命周期结束后仍无法被 GC 机制回收,导致其占用的内存空间无法得到释放
具体如何做?
- Activity 在执行销毁的时候 我们如何得知?
- 如何判断一个 Activity 无法被 GC 机制回收?
采用 ActivityLifeCycleCallbacks + WeakHashMap 的方式,我们可以不主动暴露 RefrenceQueue 这个对象,WeakHashMap 的 key 可以自动被弱引用,可以自动被回收,那么这个 key 就可以是 Activity,key 被回收了,那么 value 也就跟着被移除了,监听 Activity 的回收,也就达到监听泄露的目的了;
借助 ActivityLifeCycleCallbacks 的 onActivityDestoryed 回调,在回调中将传递过来的 Activity 放入 WeakHashMap 中,然后在 onStop 的回调中通过 GC 监测 Activity 有没有泄露;
好了,内存这块就讲到这里吧
下一章预告
继续搞内存
欢迎三连
来到来了,点个关注,点个赞吧,你的支持是我最大的动力~