Appearance
JVM / Dart 内存模型
JVM 与 Dart 的内存模型都在回答同一个根问题:对象放在哪里、谁可以访问、何时可见、何时回收、为什么会泄漏或卡顿。只是两者对共享内存与并发的取舍不同。
一句话定义
- JVM:多线程共享堆内存,靠 JMM、锁、volatile、原子类去协调可见性与并发安全
- Dart:默认以 isolate 为边界,每个 isolate 独立堆,不共享普通对象,从模型上减少共享内存竞态
为什么需要
内存模型不是“面试八股”,而是直接决定这些问题能不能说清:
- 为什么 Android 会 OOM、泄漏、GC 卡顿
- 为什么某个对象明明置空了还没释放
- 为什么 Dart isolate 默认不共享对象
- 为什么多线程共享缓存时要考虑可见性
- 为什么 Flutter 页面销毁了,但某些对象还活着
底层机制
1. JVM:线程共享堆,栈保存线程私有执行现场
经典拆法:
- 线程栈:局部变量、调用栈帧
- 堆:对象实例、数组
- 类元数据区 / Metaspace:类信息、方法元数据、常量池等
直觉理解:
- 局部变量大多在线程自己的栈里,天然线程私有
new出来的对象通常在堆里,被多个线程共享访问的风险更高
这就是为什么:
- 局部计算一般不用加锁
- 全局缓存、单例、共享状态要考虑同步与可见性
2. Dart:每个 isolate 自己一套堆和事件循环
Dart 默认不是“多个线程一起摸一个堆”,而是:
- 每个 isolate 有自己的事件循环
- 每个 isolate 有自己的堆
- isolate 之间通过消息传递通信
这带来的直接好处是:
- 少了大量共享内存锁竞争
- 少了“这个对象被另一个线程改了”的复杂度
代价是:
- 不能像 JVM 那样随便共享对象引用
- 真正要跨 isolate 通信时,需要序列化/消息传递思维
3. GC 都会停顿,但停顿来源和体感不同
JVM
你会遇到:
- Young GC / Old GC
- STW(Stop-The-World)
- 大对象、碎片、晋升压力
在 Android 里,GC 问题常表现为:
- 掉帧
- 卡顿
- 内存抖动
- OOM 前的频繁回收
Dart / Flutter
Dart 也有垃圾回收,但体感常和:
- Widget 重建频率
- 短命对象分配量
- 大图/大列表/解析任务
- isolate 是否把重任务隔离出去
一起出现。
Android / Flutter / Web / Backend 对照
| 维度 | JVM / Android | Dart / Flutter | Web / JS | Backend |
|---|---|---|---|---|
| 默认共享内存 | 多线程共享堆 | isolate 独立堆 | 单线程主上下文 | 依语言而定 |
| 线程私有执行现场 | 栈帧 | isolate 自己的调用栈 | 调用栈 | 依语言而定 |
| 并发风险重点 | 可见性、锁、竞态 | isolate 间通信、主 isolate 卡顿 | 主线程阻塞 | 数据竞争、线程池/协程治理 |
| 回收问题表现 | GC 卡顿 / OOM | UI 卡顿 / 分配抖动 / isolate 边界 | 主线程卡顿 | 吞吐下降 / 堆膨胀 |
常见场景
1. Android 页面退出了但对象还没释放
常见原因:
- 单例持有 Activity
- Handler / Runnable 持有页面引用
- Observer / callback 没解绑
- 大集合缓存未清
2. Flutter 页面没了,但控制器/订阅还活着
典型问题:
TextEditingController没dispose- stream subscription 没取消
- 长任务结果回调晚于页面销毁
3. 大 JSON / 大图解析导致卡顿
这类问题常被误以为是“异步就好了”。实际更关键的是:
- 任务是否还在主执行上下文
- 是否制造了过多短命对象
- 是否该拆到 isolate
常见坑
| 坑 | 现象 | 修法 |
|---|---|---|
以为对象置 null 就一定立刻释放 | 内存没马上降 | GC 是时机性回收,不是立刻销毁 |
| 共享对象不做可见性控制 | 偶现脏读/竞态 | JVM 下用同步原语;Dart 下优先隔离通信 |
| Flutter 里过度制造临时对象 | 掉帧、GC 抖动 | 减少不必要分配、重任务移出主 isolate |
| 把 isolate 当便宜线程池 | 通信复杂、过度设计 | 只给 CPU 密集任务上 isolate |
与相近概念对比
| 概念 | 关注点 | 更像解决什么问题 |
|---|---|---|
| JMM | 多线程共享内存可见性 | 为什么另一个线程能不能看见修改 |
| GC | 对象何时释放 | 为什么内存不立刻下降 |
| isolate | 内存隔离与消息通信 | 如何减少共享内存复杂度 |
| 生命周期 | 对象应该活多久 | 为什么页面走了对象还活着 |
对应实验
目前本主题先以理论文档为主;相关对照入口:
- Kotlin / JVM 并发:Java 并发模型总览
- Dart event loop / isolate:Dart Future / Event Loop / Isolate
复习检查题
为什么说 JVM 的共享内存复杂度通常高于 Dart isolate?
答:因为 JVM 多线程默认共享堆,对象可被多个线程同时读写,需要额外处理可见性与同步;Dart isolate 默认独立堆,不共享普通对象,天然减少了共享内存竞态。
为什么页面销毁后对象还可能活着?
答:因为只要还有引用链,GC 就不会回收;页面结束并不等于对象自动失去所有引用。
什么情况下应优先考虑 isolate?
答:CPU 密集任务、大 JSON 解析、图片处理等主执行上下文容易卡顿的场景,而不是所有异步任务。
速记
- JVM:共享堆 + 线程栈 + 可见性问题
- Dart:isolate 独立堆 + 消息传递
- 回收是时机性,不是立刻销毁
- 泄漏本质是“引用链没断”