6、Dalvik GC优化:Concurrent GC,Partial GC,Young Generation概念,GC日志分析。
聊到Android的内存管理,GC(垃圾回收)是个绕不开的话题。很多开发同学一看到GC日志就头大,觉得那是系统工程师才该关心的事。其实不然,我个人的经验是,搞懂GC的基本原理,能帮你写出更流畅、更省内存的代码。
Dalvik虚拟机早期用的是Stop-The-World式的GC,说白了就是GC一启动,所有线程都得停下来等它干完活。这在单核时代还能忍,到了多核大屏时代,稍微卡顿一下用户都能感觉到。所以后来ART和Dalvik都引入了更先进的GC策略。
核心观点:GC优化的目标不是消灭GC,而是让GC的停顿时间短到用户感知不到。
6.1 Concurrent GC:边干活边打扫
Concurrent GC,顾名思义,就是并发执行。GC线程和你的应用线程可以同时运行。这听起来很美好,但实现起来有不少坑。
我记得在优化一个视频播放应用时,发现播放器在滑动进度条时偶尔会掉帧。抓了GC日志一看,全是Full GC在作祟。换成Concurrent GC后,掉帧问题明显缓解了。
Concurrent GC的核心思路是:
- 标记阶段并发:GC线程标记存活对象时,应用线程可以继续分配对象。
- 短暂停顿:只在开始和结束时有短暂的暂停,用于同步状态。
- 写屏障:通过写屏障技术,记录并发期间对象引用的变化。
你想想看,如果GC期间应用完全不能动,那体验得多糟糕。Concurrent GC就是解决了这个问题。
小提示:在Android 4.4及以上版本,ART默认使用Concurrent GC。如果你的应用还在用Dalvik,可以考虑在AndroidManifest中声明android:largeHeap="true"来减少GC频率。
6.2 Partial GC:只扫一半,效率翻倍
Partial GC是Dalvik里一个很实用的优化。它不像Full GC那样扫描整个堆,而是只扫描一部分。说白了,就是偷懒但偷得聪明。
Partial GC主要分两种:
| 类型 | 扫描范围 | 适用场景 |
|---|---|---|
| Partial GC | 只扫描Young Generation | 大部分临时对象都在这里 |
| Full GC | 扫描整个堆 | 内存压力大时触发 |
我曾经在分析一个图片加载库的内存问题时发现,它频繁触发Full GC。原因是它把大量Bitmap缓存到了Old Generation。后来改成使用LruCache配合软引用,Partial GC就能搞定大部分回收工作,性能提升很明显。
注意:Partial GC虽然快,但如果Old Generation里垃圾太多,最终还是逃不过Full GC。所以不要以为有了Partial GC就可以随意创建大对象。
6.3 Young Generation:新生代,短命鬼的聚集地
Young Generation这个概念是从Java虚拟机借鉴过来的。Dalvik和ART虽然没有严格意义上的分代,但内部实现上是有类似思想的。
Young Generation的特点:
- 对象存活率低:大部分对象创建后很快就变成垃圾。
- 回收频率高:因为对象死得快,所以需要频繁回收。
- 回收成本低:只扫描一小块区域,速度很快。
为什么会这样?你想想看,一个Activity里创建的临时变量、循环中的中间对象,大多活不过几帧。把这些对象集中管理,GC效率自然就高了。
我习惯在写代码时注意以下几点:
- 避免在循环中创建大对象:比如在onDraw里new Paint,这会让Young Generation压力山大。
- 使用对象池:对于频繁创建和销毁的对象,比如Message、Rect,用对象池复用。
- 注意匿名内部类:它们会隐式持有外部类的引用,容易导致对象晋升到Old Generation。
避坑指南:我曾经在写一个列表滑动优化时,发现每次快速滑动都会触发大量GC。排查后发现是ViewHolder里创建了太多临时String对象。改用StringBuilder后,GC次数直接降了一半。
6.4 GC日志分析:读懂系统的悄悄话
GC日志是系统给你的反馈,读懂它就能知道内存的健康状况。在Android中,可以通过adb logcat查看GC日志。
一个典型的Dalvik GC日志长这样:
D/dalvikvm( 1234): GC_CONCURRENT freed 2048K, 35% free 3820K/5896K, paused 2ms+3ms, total 35ms
我们来拆解一下:
| 字段 | 含义 | 我的解读 |
|---|---|---|
| GC_CONCURRENT | GC类型 | 并发GC,说明系统在努力减少停顿 |
| freed 2048K | 回收了多少内存 | 这次回收了2MB,还不错 |
| 35% free 3820K/5896K | 堆使用情况 | 堆总大小5.8MB,空闲35%,约3.8MB |
| paused 2ms+3ms | 暂停时间 | 两次暂停,总共5ms,用户基本无感 |
| total 35ms | 总耗时 | 从开始到结束35ms |
ART的日志稍微有点不同:
I/art( 5678): Background partial concurrent mark sweep GC freed 1047KB, 42% free 8MB/14MB, paused 1.636ms, total 27.342ms
这里多了个"Background",说明是后台GC。ART会把GC分为前台和后台,前台GC要求更短的暂停时间。
实用技巧:我一般关注两个指标:paused时间和free百分比。如果paused超过10ms,或者free低于20%,就该检查内存问题了。
6.5 知识体系总览
下面这张图总结了本章的核心概念和它们之间的关系:
这张图把三个核心概念串起来了。Concurrent GC解决的是停顿时间问题,Partial GC解决的是扫描范围问题,Young Generation解决的是对象管理问题。三者配合,才能让GC既快又准。
嗯,到这里你应该对Dalvik的GC优化有了一个整体的认识。记住,GC不是敌人,而是帮你管理内存的管家。理解它的工作方式,你就能写出更高效、更流畅的Android应用。
公众号:蓝海资料掘金营,微信deep3321