Java堆内存与垃圾回收:堆结构、GC算法与分代回收
聊到Android内存管理,Java堆绝对是绕不开的核心。我经常跟团队说,搞懂了堆,你就搞懂了内存的一半。今天咱们就好好掰扯掰扯Java堆的结构、垃圾回收算法,还有那个经典的分代回收机制。
Java堆的结构:不只是一个大仓库
Java堆说白了就是一块大内存区域,用来存放对象实例。但你别以为它就是个简单的仓库,它内部是有精细划分的。
我个人习惯把堆分成三个主要区域:
- 新生代(Young Generation):存放刚创建的对象。这里对象生命周期短,回收频率高。
- 老年代(Old Generation):存放长期存活的对象。经过多次GC还活着的,就会晋升到这里。
- 元空间(Metaspace):存放类元数据。注意,它不在堆内,是独立的内存区域。
重要:新生代又细分为Eden区和两个Survivor区(S0和S1)。默认比例是8:1:1。这个比例我建议你不要轻易动,除非你做过详细的压测分析。
你想想看,为什么要把堆分成这么多块?其实就是为了提高GC效率。如果所有对象都混在一起,每次GC都要扫描整个堆,那性能就崩了。
GC算法:三种经典方案
垃圾回收算法,说白了就是怎么找到垃圾、怎么清理垃圾。我经历过三种主流算法,各有各的适用场景。
标记-清除算法
这是最基础的算法。分两步走:先标记出所有存活对象,然后清除掉未被标记的对象。
// 伪代码示意
mark(rootSet); // 从根对象开始标记
sweep(heap); // 遍历堆,清除未标记对象
我在项目中遇到过一个问题:标记-清除会产生大量内存碎片。你想想看,清除掉一些对象后,内存就像被狗啃了一样,东一块西一块。这时候分配一个大对象,明明总内存够,但就是找不到连续的空间。
避坑指南:我曾经在一个图片缓存项目里用了标记-清除,结果频繁触发Full GC,因为碎片太多导致分配失败。后来我换成了标记-整理,问题就解决了。
复制算法
这个算法把内存分成两块,只用其中一块。当这块用完了,就把存活对象复制到另一块,然后一次性清理整块内存。
嗯,这里要注意:复制算法没有碎片问题,但代价是浪费了一半内存。所以它特别适合新生代——因为新生代对象存活率低,复制成本小。
我的经验:Android ART虚拟机在新生代用的就是优化版的复制算法。它把Eden区和两个Survivor区配合使用,避免了50%的内存浪费。实际效果很不错,我测过几次GC暂停时间,基本都在5ms以内。
标记-整理算法
这个算法是标记-清除的升级版。标记完存活对象后,不直接清除,而是把所有存活对象向一端移动,然后清理边界以外的内存。
为什么会这样设计?说白了就是为了解决碎片问题。移动对象虽然有点开销,但换来的是连续的内存空间。老年代就特别适合用这个算法,因为老年代对象存活率高,复制算法的成本太高了。
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 标记-清除 | 实现简单,不需要移动对象 | 产生碎片,分配大对象困难 | 老年代,对碎片不敏感的场景 |
| 复制 | 无碎片,分配高效 | 浪费内存,存活率高时效率低 | 新生代,对象存活率低 |
| 标记-整理 | 无碎片,内存利用率高 | 移动对象有开销,需要STW | 老年代,对碎片敏感的场景 |
分代回收机制:为什么这么设计?
分代回收,说白了就是「看人下菜碟」。根据对象的年龄,采用不同的回收策略。
我记得刚接触Android性能优化时,有个同事问我:「为什么GC要分代?统一用一种算法不行吗?」我当时给他打了个比方:你清理房间,是每天扫一遍客厅(新生代),还是等半年才大扫除一次(老年代)?显然,不同区域用不同频率,效率最高。
分代回收的核心思想:
- Minor GC:发生在新生代。频率高,速度快。大部分对象在这里就被回收了。
- Major GC:发生在老年代。频率低,速度慢。通常伴随着Minor GC。
- Full GC:整个堆都清理。最耗时,要尽量避免。
关键点:对象晋升到老年代的条件是:经过多次Minor GC仍然存活,且年龄达到阈值(默认15)。这个阈值你可以通过-XX:MaxTenuringThreshold调整,但我建议保持默认,除非你做过充分的测试。
我在优化一个短视频App时,发现Full GC频繁触发。一查,原来是大量Bitmap对象在新生代存活太久,直接晋升到了老年代。后来我调整了Bitmap的复用策略,让它们尽快在新生代被回收,Full GC次数从每分钟3次降到了每小时1次。
实战建议:你可以通过adb shell dumpsys meminfo查看App的堆使用情况。重点关注Heap Alloc、Heap Free和GC Count这几个指标。如果GC Count增长很快,说明你的对象分配太频繁了。
最后说一句,分代回收不是万能的。在Android上,ART虚拟机对分代回收做了很多优化,比如并行GC、并发标记等。但核心思想没变:让短命对象快速死亡,让长命对象安稳养老。
嗯,这一章的内容就到这里。下一章我会讲Android特有的内存管理机制,包括Shared Memory和Zygote进程的内存共享。到时候咱们再细聊。