Android内存优化一:java垃圾回收机制
Android内存优化二:内存泄漏
Android内存优化三:内存泄漏检测与监控
Android内存优化四:OOM
Android内存优化五:Bitmap优化
Memory Profiler
Memory Profiler 是 Profiler 中的其中一个版块,Profiler 是 Android Studio 为我们提供的性能分析工具,使用 Profiler 能分析应用的 CPU、内存、网络以及电量的使用情况。
进入了 Memory Profiler 界面。
点击 Record 按钮后,Profiler 会为我们记录一段时间内的内存分配情况。
在内存分配面板中,通过拖动时间线来查看一段时间内的内存分配情况
通过搜索类或者报名的方式查看对象的使用情况
使用Memory Profiler 分析内存可以查看官网:使用内存性能分析器查看应用的内存使用情况
Memory Analyzer Tool(MAT)
对于内存泄漏问题,Memory Profiler 只能提供一个简单的分析,不能够确认具体发生问题的地方。
而 MAT 就可以帮我们做到这一点,它是一款功能强大的 Java 堆内存分析工具,可以用于查找内存泄漏以及查看内存消耗情况。
- 使用 Memory Profiler 的堆转储功能,导出 hprof(Heap Profile)文件。
as 生成hprof文件无法被mat识别,需要进行转换
使用hprof-conv进行转换,hprof-conv位于sdk\platform-tools
// 前一个为as生成的hprof文件,后一个为转换后的文件
hprof-conv xxx.hprof xxx.hprof
ps:as导出hprof前最好先gc几次,可排除一些干扰
- 使用mat打开转换后的文件
Histogram 可以列出内存中的对象,对象的个数以及大小; Dominator Tree 可以列出那个线程,以及线程下面的那些对象占用的空间; Top consumers 通过图形列出最大的object; Leak Suspects 通过MA自动分析泄漏的原因。
- Histogram
Shallow Heap就是对象本身占用内存的大小,不包含其引用的对象内存,实际分析中作用不大。常规对象(非数组)的ShallowSize由其成员变量的数量和类型决定。数组的shallow size有数组元素的类型(对象类型、基本类型)和数组长度决定。对象成员都是些引用,真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],对象本身的内存都很小。
Retained Heap值的计算方式是将Retained Set(当该对象被回收时那些将被GC回收的对象集合)中的所有对象大小叠加。或者说,因为X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。
- 引用链
Path To GC Roots -> exclude all phantim/weak/soft etc. references:查看这个对象的GC Root,不包含虚、弱引用、软引用,剩下的就是强引用。从GC上说,除了强引用外,其他的引用在JVM需要的情况下是都可以 被GC掉的,如果一个对象始终无法被GC,就是因为强引用的存在,从而导致在GC的过程中一直得不到回收,因此就内存泄漏了。
List objects -> with incoming references:查看这个对象持有的外部对象引用
List objects -> with outcoming references:查看这个对象被哪些外部对象引用
- OQL:对象查询语言
使用对象查询语言可以快速定位发生泄漏的Activity及Fragment
select * from instanceof android.app.Activity a where a.mDestroyed = true
select * from instanceof androidx.fragment.app.Fragment a where a.mAdded = false
LeakCanary
使用 MAT 来分析内存问题,效率比较低,为了能迅速发现内存泄漏,Square 公司基于 MAT 开源了 LeakCanary,LeakCanary 是一个内存泄漏检测框架。
集成LeakCanary后,可以在桌面看到 LeakCanary 用于分析内存泄漏的应用。
当发生泄漏,会为我们生成一个泄漏信息概览页,可以看到泄漏引用链的详情。
初始化
// 继承ContentProvider,在应用启动时,初始化LeakCanary
internal sealed class AppWatcherInstaller : ContentProvider() {
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
InternalAppWatcher.install(application)
return true
}
监听
internal class ActivityDestroyWatcher private constructor(
private val objectWatcher: ObjectWatcher,
private val configProvider: () -> Config
) {
// 在Activity执行onActivityDestroyed时,观察它的回收状态
private val lifecycleCallbacks =
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
objectWatcher.watch(
activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
}
}
}
companion object {
// 通过application.registerActivityLifecycleCallbacks监听所有Activity的生命周期
fun install(
application: Application,
objectWatcher: ObjectWatcher,
configProvider: () -> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(objectWatcher, configProvider)
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}
检测
// #ObjectWatcher
// 在对象可达性发生更改时,垃圾收集器会将其插入到这个队列。
private val queue = ReferenceQueue()
// 受观察对象的缓存,保存受观察对象的弱引用
private val watchedObjects = mutableMapOf()
// 1\. 观察对象
@Synchronized fun watch(
watchedObject: Any,
description: String
) {
...
// 创建弱引用,watchedObject 为观察对象,即activity
val reference =
KeyedWeakReference(watchedObject,..., queue)
// 保存受观察对象的弱引用
watchedObjects[key] = reference
checkRetainedExecutor.execute {
moveToRetained(key)
}
}
// 将可回收的对象从受观察对象的缓存中移除
// 当对象变为弱可及(未被强引用),在最终确定或垃圾回收实际发生之前,会将WeakReferences入队
private fun removeWeaklyReachableObjects() {
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference?
// 从queue 取出的对象为弱可及,表示即将要回收的对象,即未发生泄漏情况
// 所以,可以从受观察对象的缓存中移除它了
if (ref != null) {
watchedObjects.remove(ref.key)
}
} while (ref != null)
}
// 2\. 清理一下已回收的对象,如果对象已被回收,则无需再走下面的流程
@Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableObjects()
// 如果已经被回收,则不会存在于缓存中
val retainedRef = watchedObjects[key]
if (retainedRef != null) {d
onObjectRetainedListeners.forEach { it.onObjectRetained() }
}
}
// #ObjectWatcher
// 获取未被回收的对象数量
val retainedObjectCount: Int
@Synchronized get() {
// 清理一下已回收的对象
removeWeaklyReachableObjects()
return watchedObjects.count { .. }
}
# HeapDumpTrigger
private fun checkRetainedObjects(reason: String) {
val config = configProvider()
// 3\. 获取未被回收的对象数量
var retainedReferenceCount = objectWatcher.retainedObjectCount
// 4\. 如果有对象未被回收,执行一次GC,然后再获取一次未被回收的对象数量
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
// 5\. 判断是否有泄漏,如果有,再判断是否需要提示
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
...
// dump 对内存
dumpHeap(retainedReferenceCount, retry = true)
}
分析
LeakCanary 会解析 hprof 文件,并且找出导致 GC 无法回收实例的引用链,这也就是泄漏踪迹(Leak Trace)。
泄漏踪迹也叫最短强引用路径,这个路径是 GC Roots 到实例的路径。
线上监控
LeakCanary 存在几个问题,不同用于线上监控功能
监控
主动触发GC,会造成卡顿
采集
Dump hprof,会造成app冻结
Hprof文件过大
解析
解析耗时过长
解析本身有OOM风险
线上监控需要做的,就是解决以上几个问题。
各大厂都有开发线上监控方案,比如快手的KOOM,美团的Probe,字节的Liko
KOOM
快手自研OOM解决方案KOOM今日宣布开源
总结一下几点:
- 无主动触发GC不卡顿
通过无性能损耗的内存阈值监控来触发镜像采集。将对象是否泄漏的判断延迟到了解析时
- 高性能镜像DUMP
利用系统内核COW(Copy-on-write,写时复制)机制,每次dump内存镜像前先暂停虚拟机,然后fork子进程来执行dump操作,父进程在fork成功后立刻恢复虚拟机运行,整个过程对于父进程来讲总耗时只有几毫秒,对用户完全没有影响。
hprof分析于裁剪
采用边缘计算的思路,将内存镜像于闲时进行独立进程单线程本地分析,不过多占用系统运行时资源;分析完即删除,不占用磁盘空间;分析报告大小只有KB级别,不浪费用户流量。
针对镜像回捞需求,对hprof进行运行时hook裁剪,只保留分析OOM必须的数据。裁剪还有数据脱敏的好处,只保留对分析问题有用的内存中类与对象的组织结构,并不上传真实的业务数据,充分保护用户隐私。