5、Dalvik垃圾回收基础:Mark-Sweep算法,GC触发条件,STW(Stop-The-World)现象
聊到Android内存管理,垃圾回收(GC)是个绕不开的话题。我记得刚入行那会儿,一遇到应用卡顿,第一反应就是“是不是GC了?”后来慢慢深入底层,才发现GC这事儿远比想象中复杂。今天咱们就来扒一扒Dalvik时代的垃圾回收基础——Mark-Sweep算法、GC触发条件,还有那个让人头疼的STW现象。
5.1 Mark-Sweep算法:最经典的GC策略
Dalvik虚拟机早期用的就是Mark-Sweep(标记-清除)算法。说白了,这算法分两步走:先标记出哪些对象是“活的”,再把没标记的统统清掉。
标记阶段(Mark Phase)
从GC Roots出发,遍历所有可达对象。GC Roots包括:
- 栈帧中的局部变量
- 静态变量
- JNI全局引用
- 活跃线程
遍历过程中,每个被访问到的对象都会被标记为“存活”。
清除阶段(Sweep Phase)
遍历整个堆,把没标记的对象回收掉。注意,这里只是把内存块标记为空闲,不会做压缩整理。所以Mark-Sweep之后,内存碎片是难免的。
核心要点:Mark-Sweep算法最大的问题就是内存碎片。我在项目中遇到过,一个应用跑久了,明明总内存还有不少,但就是分配不出一块连续的大内存,直接OOM了。这就是碎片惹的祸。
5.2 GC触发条件:什么时候开始回收?
Dalvik的GC触发条件其实挺直接的。我总结下来,主要有这么几种情况:
- 堆内存不足:当分配新对象时,如果堆的空闲空间不够,就会触发GC。这是最常见的触发场景。
- 显式调用:代码里调用了
System.gc()或Runtime.gc()。不过说实话,这玩意儿只是“建议”,虚拟机不一定买账。 - 堆阈值到达:Dalvik会监控堆的使用率,当达到某个阈值(比如75%)时,会主动触发GC。
- 应用切换:当用户切换到后台时,系统可能会触发GC来回收资源。
我的经验:千万别依赖显式GC调用。我曾经在一个项目里看到有人到处写System.gc(),结果不仅没解决问题,反而因为频繁GC导致卡顿。GC这事儿,交给虚拟机自己判断就好。
| 触发场景 | 触发频率 | 影响程度 |
|---|---|---|
| 堆内存不足 | 高 | 高(可能导致OOM) |
| 显式调用 | 低(取决于代码) | 中(可能浪费CPU) |
| 堆阈值到达 | 中 | 中(可预测) |
| 应用切换 | 低 | 低(后台执行) |
5.3 STW(Stop-The-World)现象:GC的“暂停时刻”
STW,全称Stop-The-World。说白了,就是GC执行的时候,所有应用线程都得停下来。你想想看,这得多影响用户体验?
为什么需要STW?
因为GC在标记和清除的过程中,需要保证堆内存的状态是“静止”的。如果应用线程还在跑,一边分配新对象,一边GC在回收,那不乱套了?
STW的影响
- 应用线程暂停,用户能感知到卡顿
- 暂停时间取决于堆大小和存活对象数量
- 在Dalvik上,一次GC的STW时间可能在几十到几百毫秒
注意:STW时间过长,会导致应用出现明显的“掉帧”现象。我在优化一个视频播放应用时,就发现GC暂停时间经常超过200ms,直接导致播放卡顿。后来通过减少对象分配、优化数据结构,才把STW时间降下来。
5.4 如何减少STW的影响?
嗯,这里要注意。虽然Dalvik的GC设计上有些局限,但我们还是有一些办法来减轻STW带来的影响:
- 减少对象分配:对象分配越少,GC触发频率越低。多用对象池、复用对象。
- 避免在UI线程做大量分配:把内存密集型操作放到后台线程。
- 使用合适的数据结构:比如用SparseArray代替HashMap,减少对象开销。
- 监控GC日志:通过logcat查看GC频率和暂停时间,定位问题。
一句话总结:Mark-Sweep是Dalvik GC的基石,STW是它的“阿喀琉斯之踵”。理解这些基础,你才能写出更流畅的Android应用。
说实话,Dalvik的GC设计在当年已经算不错了。但到了ART时代,GC策略有了翻天覆地的变化。不过那是后话了,先把这些基础吃透,后面学起来才不费劲。
公众号:蓝海资料掘金营,微信deep3321