【安卓笔记】OOM与内存优化
0. 环境:
电脑:Windows10
Android Studio: 2024.3.2
编程语言: Java
Gradle version:8.11.1
Compile Sdk Version:35
Java 版本:Java11
1.什么是OOM
OOM即 OutOfMemoryError 内存溢出错误。常见于一些
- 资源型对象未关闭
- 注册对象未注销(反注册)
- 类的静态变量持有大量数据对象
- 单例造成的内存泄漏
- 非静态内部类的静态实例
- Handler临时性内存泄漏
- 容器中的对象没清理造成的内存泄漏
- WebView
- 使用ListView时造成的内存泄漏
等导致的。
Android内存泄漏常见场景及解决方案
- 资源性对象未关闭
常见场景:例如:Bitmap,使用后就不管了。
解决方案:对于资源性对象不再使用时,应该立即close(),然后设置为null。例如Bitmap。如果未关闭则容易造成内存泄漏。最好是在activity中的onDestroy()中将Bitmap给close()并设置成null。
- 注册对象未注销
常见场景:例如:BroadcastReceiver、EventBus未注销造成的内存泄漏。
解决方案:应该在activity的onDestroy()中及时注销/反注册
- 类的静态变量持有大数据对象
常见场景:静态变量存储数据
解决方案:尽量避免使用静态变量存储数据。如果是大数据对象,建议使用数据库存储。
关于数据库,可以查看我的这篇文章:【安卓笔记】Room数据库的基本使用-CSDN博客
- 单例造成的内存泄漏
常见场景:使用了Activity的Context,但是被外部持有,导致无法回收。
解决方案:优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后在使用到的地方从弱引用中获取Context。如果获取不到,则return即可。
- 非静态内部类的静态实例
常见场景:实例化了 非静态内部类的静态实例
解决方案:该实例的生命周期和应用一样长,这就会导致该静态实例一直持有该Activity的引用,activity的内存资源不能正常回收。此时,我们可以将该内部类设置为静态内部类或者将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application的Context。如果一定要使用Activity的Context,记得用完后要置空,让GC可以回收。
- Handler临时性内存泄漏
常见场景:Handler是非静态,并且创建在Activity或者Service中。
解决方案:Message发出之后存储在MessageQueue中。在Message中存在一个target,它是Handler的一个引用。Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列实在一个Looper线程中不断地轮询处理消息。当这个Acitivity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
- 容器中的对象没清理造成的内存泄漏
解决方案:在退出程序之前,将集合.clear(),然后设置为null,再退出程序。
- WebView
常见场景:WebView大多都存在内存泄漏的问题。
解决方案:在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个单独的进程,使用AIDL与应用的主进程进行通信。WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
- 使用ListView时造成的内存泄漏
解决方案:在构造Adapter时,使用缓存的convertView
OOM比较难找到问题的根源,就是因为大部分OOM的问题,都是内存先被吃完了,最后由正常代码引发OOM,所以,日志中基本上只能看到最后的触发导致。这往往不是根本原因。
OOM的原因分类:
1. Java堆内存溢出(内存不够)
2. 无足够连续内存空间(内存够,但大部分都是碎片化)
3. FD数量超出限制(文件句柄(FD: 文件句柄)泄漏)
4. 线程数量超出限制(线程数量泄漏)
5. 虚拟内存不足
2. 一些散装知识
Java的对象生命周期
Java的对象生命周期(Java Object Life Cycle)(有大概了解就行了)
Created 创建
in use 应用
Invisible 不可见
Unreachable 不可达
Collected 收集
Finalized 终结
Deallocated 对象空间重新分配
Java的四种引用
强引用:=
强引用:被GC扫描到了,也不会回收。容易造成OOM
软引用:SoftReference
软引用:内存不足时,会回收
弱引用:WeakRefoerence
弱引用:GC扫描到了,会被回收掉
虚引用:PhantomReference
虚引用:相当于没有被引用
3. Android内存分析命令
- dumpsys meminfo
- procrank
- cat/proc/meminfo
- free
- showmap
- vmstat
- top -n 1
3.0 一些条件
1. 手机需要root(Android模拟器也行)
2. 需要使用adb
3. 进入 adb shell,再操作以下命令
3.1 dumpsys meminfo
功能:大概判断哪个页面有内存泄漏
内存指标概念
item | 全称 | 含义 | 等价 |
USS | Unique Set Size | 物理内存 | 进程独占的内存 |
PSS | Proportional Set Size | 物理内存 | PSS = USS + 按比例包含共享库 |
RSS | Resident Set Size | 物理内存 | RSS = USS + 包含共享库 |
VSS | Virtual Set Size | 虚拟内存 | VSS = RSS + 未分配实际物理内存 |
总结:VSS >= RSS >= PSS >= USS,但 /dev/kgsl-3d0部分必须考虑VSS
一般只看PSS。
这边说一下如何判断activity中的view是否有内存泄漏:
例如从Aactivity进入Bactivity,再返回再进入,多次之后。
使用dumpsys meminfo命令,在命令提示符窗口最下方找到Objects组,查看ViewRootImpl数据,如果该数据在每次activity进入时都会增加。则代表该activity持有的view内存泄漏了
3.2 procrank
功能:获取所有进程的内存使用情况,顺序以PSS的大小从大到小排列。
procrank比dumpsys meminfo,更详细输出 VSS/RSS/PSS/USS内存指标。
例如:最后一行输出以下6个指标:
total | free | buffers | cached | shmem | slab |
2857032K | 998088K | 78060K | 78060K | 312K | 92392K |
3.3 cat /proc/meminfo
功能:查看更加详细的内存信息
3.4 free
功能:查看可用内存。单位KB。
该命令比较简单、轻量,专注于查看剩余内存的情况。数据来源于3.3 cat /proc/meminfo
3.5 showmap
功能:用于查看虚拟地址区域的内存情况
用法:
showmap -a [pid]
- start addr和end addr: 分别代表进程空间的起止虚拟地址
- virtual size/RSS/PSS:具体看3.1中的介绍
- shared clean:代表多个进程的虚拟地址可指向这块物理空间
- shared:共享数据
- private:该进程私有数据
- clean:干净数据,该内存数据与disk数据一致。当内存紧张时,可直接释放内存,不需要回写到disk
- dirty:脏数据,需要回写到disk,才能被释放。
3.6 vmstat
功能:不仅可以查看内存情况,还可以查看进程运行队列、系统切换、CPU时间占比等。(可以周期性动态输出)
用法:
vmstat [ -n iterations ] [ -d delay ] [ -r header_repeat ]
解释:
-n iterations:数据循环输出的次数
-d delay:两次数据间的延迟时长(单位:s)
-r header_repeat:循环次数
3.7 top -n 1
op命令是Linux下常用的性能分析工具,可以实时显示系统中各个进程的资源占用情况。
功能:显示当前系统正在执行的进程的相关信息:进程ID、内存占用率、CPU占用率等
用法:
top [参数]
参数:
-b 批处理
-c 显示完整的治命令
-l 忽略失效过程
-s 保密模式
-S 累积模式
-i<时间> 设置时间间隔
-u<用户名> 指定用户名
-p<进程号> 指定进程
-n<次数> 循环显示的次数
3.8 总结
1. dumpsys meminfo 适用场景:查看进程的oom adj、dalvik/native等区域内存使用情况、某个进程/apk的内存情况
2. procrank 适用场景:查看进程的VSS/RSS/PSS/USS
3. cat /proc/meminfo 适用场景:查看系统的详尽内存信息,包含内核情况
4. free 适用场景:只查看系统的可用内存
5. showmap 适用场景:查看进程的虚拟地址空间的内存分配情况
6. vmstat 适用场景:周期性打印进程运行队列、系统切换、CPU时间占比等情况
4. 常见分析工具
1. Memory Analyzer Tools
开发过程中,本地分析
2. Memory Profiler(本文不介绍)
Android Studio 自带的分析工具
3. LeakCanary(本文不介绍)
线上版本集成该工具分析
5. MAT(Memory Analyzer Tools)工具的使用
工具下载地址:Downloads | The Eclipse Foundation
5.1 .hprof文件分析
5.1.1 储存.hprof文件到手机内
public static void createDumpFile(Context context) {// 目录路径String LOG_PATH = "/dump.gc/";// 文件名称SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ssss");String createTime = sdf.format(new Date(System.currentTimeMillis()));// 手机环境String externalStorageState = Environment.getExternalStorageState();// 如果有SD卡if (Environment.MEDIA_MOUNTED.equals(externalStorageState)) {// 文件路径File file = new File(Environment.getExternalStorageDirectory().getPath() + LOG_PATH);if (!file.exists()) {// 没有该路径就创建file.mkdirs();}String hprofPath = file.getAbsolutePath();if (!hprofPath.endsWith("/")) {hprofPath += "/";}// 文件名称hprofPath += createTime + ".hprof";try {// dump储存.hprof文件android.os.Debug.dumpHprofData(hprofPath);} catch (IOException e) {throw new RuntimeException(e);}}
}
在执行完需要查看是否OOM的代码后,再调用该函数,就可以在 手机路径下看到.hprof文件:
sdcard/dump.gc/yyyy-MM-dd_HH.mm.ssss.hprof
5.1.2 MAT工具打开文件
此时无法用MAT工具直接打开文件,需要转换。转换的命令如下:
hprof-conv dump1.hprof converted-dump2.hprof
解释
dump1.hprof:转换前的文件
dump2.hprof:转换后的文件
如果上面命令无效,请使用以下命令:
hprof-conv.exe dump1.hprof dump2.hprof
解释:
dump1.hprof:转换前的文件
dump2.hprof:转换后的文件
(提一嘴,该命令的环境路径在 android/sdk/platform-tools 下)
转换后就可以打开了。
5.1.3 查看.hprof文件
其中 Problem Suspect 为工具猜想泄漏可能。不重要
首先点击柱状图,按对象查看,出现下面的列表:(柱状图右侧的按钮,自行测试一下就好了。例如:按线程查看)
然后第二步选择按package
选择完这个后,界面就会变成如下:
这样,就可以比较直观、比较友好的查看包名下的对象了。
5.1.4 具体对象查看
我们在该页面下,右键鼠标,选择List objects后,右侧有两个选项,如下图:
with outgoing references
查看该对象持有谁
with incoming references
查看谁持有该对象
5.2 MAT中浅堆和深堆
Shallow Heap:浅堆
对象本身占用的内存
Retained Heap:深堆
统计结果:本身占用内存 + 引用的对象占用内存
例如下图:
假设ABCDEFG各占用10个内存大小。但是A持有BC,B持有DE,C持有FG。所以A的浅堆为自身:10,深堆为70(A自身+BDE引用 +CFG引用)
在工具中,就是这一部分数据:
5.2.1 找到深堆异常
通过对深堆的查看,就可以知道 哪个对象占用内存过大。然后通过incoming和outging的引用关系查看,就可以查询上下关系,找到内存泄漏的点。例如某个对象被多处引用,被持久化导致无法释放。