Appearance
内存与 GC:对象布局、并发拷贝回收与低内存链
本页导读:本页回答四个问题:一个 Java 对象在 ART 堆里到底占多少字节;GC 如何做到「边搬对象边跑业务」;native 内存在 malloc/Scudo 层面归谁管;以及从 onTrimMemory 到 LMKD 的整条低内存链。前置:ART 篇、进程模型。
一、对象内存布局:从 8 字节头说起
ART 对象头(32/64 位下都以紧凑为目标)主要装两样东西:
- lock word(monitor/哈希/GC 状态位打包) 与 压缩引用(compressed class pointer)——64 位设备上引用默认按 32 位压缩(boot image 基址 + 4GB 假设),这是 ART 省内存的第一刀;
- 数组多一个 length 字段;实例字段按类型分组 + 对齐重排(字段序优化,long 归 long 区),父类字段在前。
估算口径(口诀版,具体随版本微调):
| 组成 | 大小 |
|---|---|
| 对象头 | 8 字节 |
| 引用字段 | 4 字节(压缩) |
| 原始字段 | 按类型对齐 |
| 整对象 | 8 字节对齐补齐 |
工程推论:Long/Integer 装箱一个值=对象头+字段+对齐,常是裸值的 4 倍成本;集合里 HashMap<Long,Boolean> 这类结构是隐形放大器,AndroidX 的 LongSparseArray/SparseIntArray 家族存在的理由就写在这个表里。
kotlin
// 一个「不起眼」的热点容器
val cache = HashMap<Long, Long>() // 每个 entry ≈ Node(对象头+3引用) + 两个装箱
// 等价的省法:
val cache2 = androidx.collection.LongLongSparseMap() // 两个 long 数组,零对象二、GC 演进与并发拷贝(CC)
Dalvik 时代 STW 复制式 GC 一停几百 ms 是卡顿祖传来源。ART 的主线是 Concurrent Copying(Android 5 引入,此后持续强化):
text
CC 的心跳(概念版):
1. 标记阶段:与 mutator 并发,读屏障(read barrier)盯住「改引用」
2. 转场(flip):极短 STW,所有线程在安全点换「旧→新」转发表
3. 复制/清扫:对象搬家(去碎片)与业务并发,屏障兜住访问旧地址的手
4. 回收空间归还分配器配套机制按版本累积:Region 分配栈(TLAB 化,Android 8/9 时代)、后台 GC(BackgroundSweeper,空闲时清 Zygote 堆)、GC record & replay(记录 Zygote 预热期分配,子进程重放或直接吃 app image,Android 10/11 时代)——「多进程省内存」的底层叙事一半在 GC 侧。
堆空间版图(决定对象「住哪、怎么被搬」):
| Space | 装什么 | GC 待遇 |
|---|---|---|
| image space | boot image 预置对象 | 不搬(只读共享) |
| zygote space | Zygote 预加载对象 | 只读共享,回收极保守 |
| main space | 常规分配 | CC 搬移主战场 |
| large object space | 大数组/大对象 | 基本不搬移,直接回收 |
| app image space | 启动对象快照 | mmap 恢复,配合 record-replay |
触发点(logcat 里 GC 原因的官方口径,字段名以版本为准): Alloc(分配失败)|GC limiter exceeded(分配超限)|Background(空闲清扫)|Explicit(System.gc 被限频后的礼貌版)|Sweepsystem/TrimMemory 联动|Concurrent(堆水位达标自动)。
三、分配路径:快慢两条道
- 快路径:编译器把分配内联进方法体——从线程本地分配栈(TLAB/region alloc stack)指针碰撞即得;无锁、纳秒级。
- 慢路径:栈不够→申请新 region;失败→触发 GC→仍失败→
OutOfMemoryError(Java 层的 OOM 绝大多数是堆上限问题,不是物理内存没了)。 - 大对象直入 LOS;数组超限、
byte[]解码大图是 OOM 三兄弟之二(第三个是 Bitmap 未采样)。 - 显式
System.gc()被平台刻意弱化(限频 + 厂商可无视)——需要「主动归还」请用VMRuntime/trim 回调的正确通道,而不是玄学调 GC。
java
// frameworks/base/core/java/dalvik/system/VMRuntime.java(能力面节选大意)
public final class VMRuntime {
public void requestConcurrentGC() { ... } // 框架内部用的「礼貌请求」通道
public void notifyStartupCompleted() { ... } // 启动完成信号 → GC 策略切换
public native void nativeGc(int gen, boolean force); // 仅供框架
}内存账本怎么看:adb shell dumpsys meminfo com.example(PSS/RSS 分段:Java Heap/Native Heap/Code/Graphics);Android Studio Memory Profiler 看分配火焰与 GC 事件对齐;Debug.getMemoryInfo 拿同一份账。
bash
adb shell dumpsys meminfo com.example | head -25
# 重点行:Java Heap / App Summary: PSS Total / Native Heap /
# Graphics(含 GL/Skia)/ Code(dex/oat/so 映射)四、Native 内存:Scudo 时代
- Java 堆由 ART 管;native 堆归分配器:Android 11 起默认 Scudo(硬化版 malloc:每个 chunk 加校验、自动清零,主打防堆溢出利用,代价是略慢与略高水位),之前是 jemalloc。
- 观察工具链:
malloc debug(wrap.sh 挂到进程查泄漏/越界)、heapprofd(Perfetto 数据源,native 采样分配栈,定位「native 只涨不降」神器)、dumpsys meminfo的 Native Heap 行。 - Bitmap 像素自 Android 8 起在 native 堆(Graphics/native 段可见)——「Java 堆没涨但 PSS 爆」的经典真相。
- 硬标签 MTE(Android 12+,Pixel 6+ 起)在硬件层给指针打 tag 拦 use-after-free——native 稳定性从「事后查」走向「当场炸」,灰度开启时崩溃率会「变差」,那是真 bug 现形。
malloc debug 挂进目标进程(userdebug/root 常用):
bash
adb shell setprop wrap.com.example \
"-d libwrapshim.so -e MALLOC_DEBUG=1 -e malloc.returns_null=false"
# 具体参数族以 bionic/docs/malloc_debug 为准五、泄漏:模式、检测与 LeakCanary 原理
高频泄漏模式(每个都对应一条强引用链):
| 模式 | 引用链 | 修复 |
|---|---|---|
| 静态集合持有 Context | static Map → Activity | 用 applicationContext / 弱引用 |
| 非静态内部类 Handler/Listener | lambda → 外部 Activity → MessageQueue 中消息 | 静态内部类 + 弱引用 + 生命周期清理 |
| 未反注册监听 | 系统服务/EventBus 表 → 页面 | onDestroy 成对解注册 |
| 线程/协程泄漏 | 线程持有闭包,线程池不清 | 生命周期作用域(viewModelScope 等) |
| WebView/地图类重资源 | 单例缓存 View | 显式 destroy + 不用单例持 View |
LeakCanary 原理一句话:「Activity 销毁后强引用还在 = 泄漏」的自动化审判。
kotlin
// 其核心机制(简化):
val watcher = KeyedWeakReference(activity, uuid, description, queue)
// 1. onDestroy 后触发几次 GC(主动跑几轮给足机会)
// 2. 观察 ReferenceQueue:被回收 → 无罪释放
// 3. 超时未被回收 → dump hprof → 解析对象图
// 4. 从 GC Root 找「最短保留路径」→ 输出你看得懂的引用链被泄漏方的标准防御模板(Handler 泄漏一代目经典):
kotlin
class SafeFragment : Fragment() {
private class H(f: SafeFragment) : Handler(Looper.getMainLooper()) // 仅弱引用语义见下
private val h = object : android.os.Handler(Looper.getMainLooper()) {}
override fun onDestroyView() {
h.removeCallbacksAndMessages() // 清掉队列里攥着自己的消息
super.onDestroyView()
}
}工程注意:检测只在 debug 构建跑;release 想带内存防线,用 OnLowMemory/trim 上报 + heapprofd 抽样,别上完整 LeakCanary。
六、低内存链:从 trim 回调到 LMKD
一个 cached 进程内存压力下的完整遭遇(与进程模型页衔接):
- 系统水位紧 →
onTrimMemory(TRIM_MEMORY_UI_HIDDEN/RUNNING_CRITICAL/COMPLETE):应用侧主动弃缓存(图片池、预热资源)——这是免费的生命权; - 继续恶化 → LMKD 按 PSI 颠簸度选杀(先高 adj、综合占用与 thrashing),写事件日志;
- 再恶化 → 内核 OOM killer 兜底;
- 应用退出原因可查:
ActivityManager#getHistoricalProcessExitReasons(API 30+)区分 LMK/崩溃/ANR/用户划掉; - 设计启示:把「会被杀」写进架构假设——可重建、可恢复、可丢缓存,才是 Android 内存模型下的好公民。
kotlin
override fun onTrimMemory(level: Int) {
when (level) {
TRIM_MEMORY_UI_HIDDEN -> imageCache.trimTo(0.5f) // 让出一半
TRIM_MEMORY_RUNNING_CRITICAL -> imageCache.clear() // 全力自保
else -> {}
}
}七、常见误区问答
Q1:System.gc() 到底有没有用? 没有直接有用:平台限频且可能被合并;它的正确替身是「给 GC 制造机会」——LeakCanary 也是跑多轮 + 等 ReferenceQueue,不是指望一次回收一切。
Q2:adb shell dumpsys meminfo 的 PSS 和 Java 堆对不上? 正常:PSS 含 native、图形、代码映射与共享摊薄;查 Java 泄漏看 Debug MemoryInfo 的 Java Heap 与 hprof,别拿 PSS 直接归因 Java。
Q3:内存泄漏一定会 OOM 吗? 不一定,更常见结局是被 LMK 提前杀掉——泄漏的真实成本是「存活时间短 + 冷启动多」,线上要用退出原因统计衡量,不能只盯崩溃率。
版本注记:基于 Android 16 / API 36;Android 5 差异:CC GC 登场;8/9 差异:region 分配 + Bitmap 像素入 native;10/11 差异:GC record-replay 与 app image、Scudo 默认化;12+ 差异:MTE/heapprofd 工具面成熟;13~16 差异:LMK 与 freezer 策略持续演进(以 AOSP 与设备为准)。
本页小结:对象以 8 字节头 + 压缩引用精打细算;GC 用读屏障与转场实现「边搬边跑」的并发拷贝;分配走 TLAB 快路径、OOM 多是堆上限;native 内存交给硬化过的 Scudo,用 heapprofd 看它;泄漏靠引用链思维 + LeakCanary 的自动化审判;低内存链上,主动 trim 是应用唯一能主动出牌的一手。
参考链接
- https://developer.android.com/topic/performance/memory
- https://developer.android.com/topic/performance/memory-analysis
- https://source.android.com/docs/runtime? (GC 演进章节,以站点导航为准)
- https://cs.android.com/android/platform/art/+/main:runtime/gc
- https://cs.android.com/android/platform/art/+/main:runtime/gc/allocator/rosalloc.cc
- https://source.android.com/docs/runtime
- https://cs.android.com/android/platform/frameworks/base/+/main:core/java/dalvik/system/VMRuntime.java
- 站内:ART、进程模型、ANR