1、Kotlin内存管理概述:JVM内存模型、Kotlin与Java内存管理的异同、GC基础原理
各位同学好,我是你们的老朋友。今天咱们正式开讲Kotlin内存管理。
说实话,我见过太多Android开发者,Kotlin写得飞起,协程、高阶函数信手拈来。但一遇到OOM或者内存泄漏,就抓瞎了。为什么?因为底层的内存管理机制没搞懂。
这一章,咱们先把地基打牢。我会从JVM内存模型讲起,再对比Kotlin和Java在内存管理上的差异,最后聊聊GC的基本原理。嗯,内容有点干,但都是硬通货。
1.1 JVM内存模型:你的数据到底住哪儿?
先问大家一个问题:你在代码里new出来的对象,到底存在哪里?
JVM的内存区域,说白了就分这么几块:
| 区域名称 | 存什么 | 谁在用 | 会不会OOM |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | 所有线程 | 会,最常见 |
| 栈(Stack) | 局部变量、方法调用 | 单个线程 | 会,递归太深 |
| 方法区(Method Area) | 类信息、常量、静态变量 | 所有线程 | 会,加载类太多 |
| 程序计数器 | 当前线程执行的字节码行号 | 单个线程 | 不会 |
| 本地方法栈 | Native方法调用 | 单个线程 | 会 |
我个人习惯把堆想象成一个巨大的仓库,所有对象都堆在里面。栈呢,就像每个工人手里的记事本,记着当前干到哪一步了。
这里有个坑,我必须要说——栈上分配。Java和Kotlin里,有些小对象其实可以分配在栈上,不用进堆。但Android的ART虚拟机实现得并不彻底。我在项目中遇到过,以为小对象不占堆内存,结果一查MAT,堆里全是小对象。嗯,别太乐观。
1.2 Kotlin与Java内存管理的异同
很多人觉得Kotlin就是Java的语法糖,内存管理应该一样吧?
大错特错。
Kotlin编译器在背后做了很多“小动作”,直接影响内存布局。我列几个关键差异:
1.2.1 空安全与装箱
Java里,int就是int,直接存值。但Kotlin里,如果你写Int?,它会被装箱成Integer对象。
// Java
int a = 10; // 栈上,4字节
// Kotlin
val a: Int = 10 // 还是基本类型,没问题
val b: Int? = 10 // 装箱成Integer对象,堆上分配
我曾经在优化一个列表页面时,发现内存暴涨。查了半天,原来是把所有Int都声明成了可空类型。每个数字都变成了对象,能不涨吗?
1.2.2 Lambda与匿名内部类
Java里写匿名内部类,每次都会生成一个新的class文件。Kotlin的Lambda呢?
Kotlin编译器会尽量把Lambda内联掉。但如果Lambda捕获了外部变量,它就会生成一个匿名内部类。而且,Kotlin的Lambda默认就是对象。
// 这个Lambda每次调用都会创建新对象
list.forEach { item ->
println(item)
}
// 如果改成这样,就不会创建对象
inlineList.forEach { item ->
println(item)
}
说白了,inline关键字是Kotlin给我们的性能武器。我建议,高频调用的Lambda,能inline就inline。
1.2.3 伴生对象与静态变量
Java的静态变量直接存在方法区。Kotlin的伴生对象呢?它其实是一个真正的对象,存在堆里。
class MyClass {
companion object {
val TAG = "MyClass"
}
}
// 反编译后,TAG其实存在MyClass.Companion这个对象里
这意味着什么?伴生对象里的变量,访问时多了一层对象引用。虽然影响不大,但如果你有大量静态常量,用const val可以避免这层包装。
1.3 GC基础原理:垃圾是怎么被回收的?
GC,全称Garbage Collection,垃圾回收。说白了就是JVM帮你打扫内存卫生。
Android用的是ART虚拟机,它的GC和标准JVM不太一样。但基本原理是相通的。
1.3.1 引用计数法
最简单粗暴的方法。每个对象记一个数字,被引用就+1,引用失效就-1。数字变成0,就回收。
听起来很完美对吧?但有个致命问题——循环引用。
A引用B,B引用A,两个对象都没人用了,但引用计数都是1。这就成了内存泄漏。
所以,JVM不用引用计数法。
1.3.2 可达性分析
这才是JVM真正用的方法。从一组“根对象”(GC Roots)出发,沿着引用链往下走。走不到的,就是垃圾。
GC Roots包括:
- 栈帧里的局部变量
- 静态变量
- JNI引用
- 活跃线程
你想想看,如果一个对象从GC Roots出发找不到,那它肯定没人用了。回收它,没毛病。
1.3.3 分代回收
这是GC的核心理念。对象分两种:
- 新生代:刚出生的对象,朝生夕死。GC频率高,但每次很快。
- 老年代:熬过几次GC还没死,就搬到这里。GC频率低,但一次很慢。
Android ART里也有类似设计,但分代策略更复杂。我记得有一次,我优化一个图片加载库,发现大量Bitmap对象直接进了老年代。为什么?因为Bitmap太大,新生代装不下。这就导致老年代GC频繁触发,界面卡顿。
1.4 小结
这一章咱们聊了JVM内存模型、Kotlin和Java的差异、GC基本原理。说白了,Kotlin的内存管理比Java更“重”一些——空安全、Lambda、伴生对象,都在背后多做了事。
但别怕。知道问题在哪,就能解决问题。下一章,咱们会深入Kotlin对象的内存布局,看看一个Kotlin对象到底占多少字节。
好,今天就到这儿。我是你们的性能优化老司机,咱们下章见。