1. 内存管理基础:Java堆内存结构、GC回收机制、内存泄漏概念

各位同学,咱们今天聊聊内存管理。说实话,这玩意儿是Android性能优化的根基。你想想看,一个App如果内存管理做得不好,那卡顿、闪退、OOM就是家常便饭。我这些年排查过的线上问题,十有八九都跟内存沾边。

1.1 Java堆内存结构

先说说堆内存长什么样。Java堆,说白了就是一块大内存区域,用来存放对象实例。它被分成了几个区域:

  • 年轻代(Young Generation):新对象出生的地方
  • 老年代(Old Generation):熬过多次GC的对象会搬到这里
  • 元空间(Metaspace):存放类的元数据,取代了以前的永久代

年轻代内部又分了三个区:Eden区和两个Survivor区(From和To)。比例一般是8:1:1。我刚开始做Android时,总觉得这个比例是固定的,后来才发现可以通过JVM参数调整。不过说实话,大部分场景下默认值就够用了。

核心要点:对象分配优先在Eden区。Eden满了触发Minor GC,存活对象移到Survivor区。熬过15次GC还没死,就晋升到老年代。

1.2 GC回收机制

GC,全称Garbage Collection,垃圾回收。它的工作就是找到那些不再使用的对象,然后清理掉。听起来简单,但实现起来门道很多。

常见的GC算法有几种:

算法 原理 优缺点
标记-清除 先标记存活对象,再清除未标记的 会产生内存碎片
标记-复制 把存活对象复制到另一块区域 没有碎片,但浪费空间
标记-整理 标记存活对象,然后向一端移动 没有碎片,但效率稍低

Android上用的ART虚拟机,默认用的是CMS和G1。我个人习惯用G1,因为它能更好地控制GC停顿时间。不过要注意,GC不是越多越好。我曾经遇到过一个项目,频繁触发GC导致界面掉帧,最后发现是某个循环里不断创建临时对象。

避坑指南:我曾经在优化一个图片加载库时,发现每次滑动列表都会触发大量GC。排查后发现是Bitmap对象没有复用。改用BitmapPool后,GC频率降低了80%。

1.3 内存泄漏概念

内存泄漏,说白了就是对象明明不用了,但还被引用着,GC没法回收。这就像你家里堆满了不用的东西,但就是舍不得扔,最后连走路的地方都没了。

常见的泄漏场景:

  • 静态变量持有Activity引用:Activity销毁了,但静态变量还指着它
  • 内部类持有外部类引用:匿名内部类或非静态内部类,隐式持有外部类的引用
  • Handler未及时移除:MessageQueue里还有未处理的消息,Handler又持有Activity引用
  • 注册监听未反注册:比如广播、观察者模式,注册了没取消

嗯,这里要注意。内存泄漏不是一下子就能看出来的。它像温水煮青蛙,慢慢积累。你可能会发现App越用越卡,或者莫名其妙就闪退了。我遇到过最夸张的一个案例,是一个新闻App,用户连续浏览半小时后必崩。最后定位到是WebView的内存泄漏,每次打开新页面都创建新的WebView,旧的又不释放。

警告:内存泄漏的排查工具推荐使用LeakCanary和Android Studio的Profiler。LeakCanary能自动检测泄漏并给出引用链,非常实用。

1.4 如何判断是否存在内存泄漏

判断内存泄漏,我一般看三个指标:

  1. 内存只增不减:操作某个功能后,内存涨上去了,但返回后没降下来
  2. GC后内存不降:手动触发GC,内存占用纹丝不动
  3. OOM频繁出现:特别是低端机型上,OOM概率明显偏高

举个例子,你打开一个Activity,进去时内存是50MB,出来时还是50MB,这没问题。但如果进去时50MB,出来时变成80MB,而且每次操作都涨,那基本可以断定有泄漏了。

总结一下:内存管理是性能优化的基石。理解堆内存结构,掌握GC机制,识别内存泄漏,这三板斧是每个Android工程师的必修课。下一章我会讲具体怎么用工具定位泄漏,以及常见的修复方案。

好了,今天就聊到这儿。记住一句话:内存管理不是玄学,是科学。只要你理解了原理,大部分问题都能迎刃而解。