- 进程内存组成
Android 每个 App 通常运行在独立进程里,进程内存大致分为几块:
| 区域 | 说明 |
|---|---|
| Java Heap | Java/Kotlin 对象(new、数组、String、集合等),由 ART/Dalvik 管理 |
| Native Heap | C/C++、malloc、new(native)、JNI、相机、WebView、各类 Native SDK |
| Stack | 各线程栈空间 |
| Code | 映射进内存的 so、jar、dex、apk 等 |
| Graphics | 图形缓冲(Bitmap、Surface 等) |
| 其它 | mmap、Ashmem 等 |
- 系统起来后,每个app 的总内存占有都是动态的,不是固定的,根据使用情况而改变,过大或系统内存紧张时,可能被LMK(Low Memory Killer)杀掉
而每个app都是一个独立的虚拟机VM,系统会给这个进程的VM分配 相对固定的配置上限,进程总内存和 VM 内存上限 是两个概念,
进程总内存包含 Java heap(VM 管的那块),也包含 Native 等,总内存一般不是系统预先固定分配的一个数字,而是各部分加起来、再受系统整体约束,而java Heap 的内存就是在这个VM的内存上,超过了这个指定的VM的内存 就会java oom,
但是Navtiv层 的内存就不受VM管了,
所以会出现:Java heap 还不高,App 已经被杀 —— 多半是 Native / 图形内存高了,不是 Java OOM。
设备物理内存 8GB
│
├── 系统 / 其它 App
│
└── 你的 App 进程
│
├── Java Heap ← 上限例如 512MB(VM/ART 管)
├── Native Heap ← 不受 Java 512MB 限制
├── Graphics
└── 其它
│
└── 合起来 = 进程总内存(可能 800MB、1.5GB…)
│
└── 太大或系统紧张 → 可能被 LMK 杀掉
(不一定是 Java OOM)
- adb shell dumpsys meminfo +包名 可以查看当前对应的进程 内存区域 java heap,native heap,栈(stack)等内存分配情况
这里是上面关键的字段分析
关键行
| 行 | 含义 |
|---|---|
| Native Heap | Native 内存 |
| Dalvik Heap | Java 内存(ART 管理) |
| Stack | 线程栈 |
| .so / .jar / .apk mmap | 代码与资源映射 |
| TOTAL | 汇总,TOTAL PSS ≈ 进程总占用 |
关键列
| 列 | 含义 |
|---|---|
| Pss Total | 该类内存在进程 PSS 中的贡献 |
| Heap Size / Alloc / Free | heap 内部统计 |
排查 OOM / 被杀进程时:Java OOM 看 Dalvik;莫名被杀看 TOTAL PSS 和 Native/Graphics。
adb shell getprop | grep -i heap 这个命令查看 查看当前设备的 VM的设置情况
heapgrowthlimit (192m) 代表默认的每个app VM层的上限
heapsize (512m) 当你的应用在android Manifest 中设置了 android:largeHeap=“true” ,能给到的最大内存
heapstartsize (8m),刚启动时先给的 每个app VM层的内存VM虚拟机的创建new的对象有生命周期包括新声代和老年代等,Android vm 会根据可达性分析算法对这些对象进行gc root ,不是引用计数,所以优化方向是:断开不必要的引用(置 null、从集合 remove、取消监听、关流等),让对象变成不可达。
系统会对每个进程分等级,其中有,空进程,后台进程,服务进程,可见进程,前台进程,一层比一层等级变高,前台进程是最高的,当系统需要回收的时候会先回收空进程,整个系统的ActivityManagerService 会对每个进程进行评分是哪个等级,存在变量adj中.内存紧张时,LMK按优先级从低到高回收进程。
被杀不一定是 OOM,常常是系统主动回收,可以尝试吧进程等级调高来进行保活
- OOM 不只有 Java OOM,还要关注:
- .Native OOM(大图、相机、FFmpeg等)
- .进程总内存过大被 LMK 杀(meminfo 里 TOTAL PSS 高,但 Java heap 还不满)
- 出问题是要分析
- 是 泄漏(内存只涨不降)还是 峰值过高(某操作瞬间很大)?
- 是 Java 还是 Native / Graphics?
- 峰值对应什么操作?(进页面、切图、列表滑动、测量、WebView…)
- 谁占最大?(Bitmap、byte[]、String、自定义大数组…)
- 为何还被引用?(从 GC Root 跟引用链)
- 分析的工具
- 使用命令 adb shell dumpsys meminfo +包名 看 PSS、Java/Native/Graphics 内存使用情况
- Android Studio Memory Profiler 实时 Java/Native 曲线、分配记录、Heap Dump,每个对象引用链路
- LeakCanary 开发期泄漏查看内存泄漏
- Perfetto / Systrace 和 GC、卡顿一起看
总结 :
内存过高会导致 GC 频繁、卡顿、发热,严重时 Java OOM 或进程因总内存过大被系统杀掉。内存管理要点是:合理分配、避免泄漏、及时释放不再需要的引用与资源。
排查上:用 Memory Profiler 看内存随时间变化,峰值时对照业务, Heap Dump,每个对象引用链路;用 LeakCanary 开发期泄漏查看内存泄漏
优化上:依据 可达性(GC Roots 根搜索),断开多余强引用、取消监听、关闭流、清理缓存,让对象可被回收;同时关注 Native/Graphics,不只看 Java heap堆。
Android Studio Memory Profiler 的使用
- 当点击profile 的时候,会出现如图下面的画面,右边红色区域 就是看内存,
我们主要是 看 View Live Telemetry 和 Analyze Memory Usage Heap Dump 快照 ,点击Start anyway 就会开启
Past Recordings 是你对Home 每个模块点击区域操作的记录
- 我们先来使用 View Live Telemetry 如何使用
如图,因为我们只是分析内存,所以我们只需要看 MEMORY 下面部分,就是对应的当前进程内存的波动形式, 以及具体每个部分占用多少内存
最底下的蓝色模块趋势就是我们着重会看的java 内存使用部分
而左脚的删除垃圾按钮就是 点击可以进行 人为 手动 gc,一旦gc 包括人为手动gc或者进程被gc操作了,那么下吗就会有个对应时间的的垃圾图标,
Live Telemetry 能做什么、不能做什么?
Live Telemetry 是用来看内存总趋势,每个操作每个时间点每个场景对应的内存趋势波动,如果你手动强制gc 内存能大幅度下降还好,如果gc 不能下降,一直占用着,那么怀疑是有泄漏或者对象持有了大内存,那么这个时候,你想分析具体,当前对应指定的场景和时间点每个对象对象占用对谁内存,每个对象对应的引用链路是如何,
那么就需要使用工具 Analyze Memory Usage Heap Dump 快照
- Analyze Memory Usage Heap Dump 快照 ,快照的意思就是对某个时间点进行拍照记录当下场景 所有对象的内存占用情况
现在我们来学习如何在快照中看对所有对象象内存占用情况和每个对象的引用链路
现在我们进行模拟下泄漏场景,手动代码制造泄漏,
我们创建了一个单例对象MemoryScenarioManager,然后单例对象 里面有个 leakedByteArrays ,然后他添加了字节
而由于是静态的,所有你gc的时候是不会回收的,然后我们开下快照
如图,最上面显示着当前进程下,每个类占用的内存大小
Shallow Size :对象本身自己多大
Retained Size,删掉这个对象,能释放多少内存,其实你可以粗略理解为 Shallow Size是这个对象本身的大小,而Retained Size是这个对象携带的对象占用的大小
虽然不大准,Retained Size正确理解是 删掉这个对象能释放多少内存,因为删掉他,他携带的对象也自然会被删掉。
当我们点击对应的类后,会 出现下面的对应当前对象链路情况
References 就是 显示 这个对象整条链路状态
Thread (GC Root)
└── contextClassLoader
└── PathClassLoader
└── Object[]
└── Class
└── static INSTANCE
└── MemoryScenarioManager ← 你选中的对象
而 Fields 则是当前这个对象里面的变量对象是什么有哪些
如图我们点击Fields 就能看到 MemoryScenarioManager 里面有着 leakedByteArrays 这个对象,并且占用多少内存