Java 技术之垃圾回收机制
垃圾回收是 Java 非常重要的特性之一,也是面试常客。它让开发者不必自己申请、释放空间,而是由后台守护进程在合适的时候把垃圾清掉——既省事,也更不容易把堆撑爆。
下面按四个问题把机制拆开:什么是堆、什么是垃圾、怎么回收、为什么要分代。
一、什么是 Java 堆内存
堆在 JVM 启动时创建,用来放运行时数据:代码里 new 出来的对象和数组,基本都在这里。对象如果迟迟收不掉、越堆越多,最后就是堆满、内存溢出。
所以 Java 在后台挂了一个 GC 守护进程:内存紧张时它会出来,把堆里的垃圾清掉,保证程序还能跑。
二、什么是垃圾
所谓垃圾,就是不再存活的对象。判断还活不活,常见两条路:引用计数、可达性分析。
| 方法 | 怎么判活 | 问题 |
|---|---|---|
| 引用计数 | 每个对象带一个计数器,引用一次 +1,没人引用则为 0,视为死亡 | 无法处理循环引用:两个对象互相指着,计数永远不为 0,但对外已经没用。Java 不用这套 |
| 可达性分析 | 从 GC Roots 出发沿引用走,能走到的是存活对象,走不到的是垃圾 | 主流方案,Java 用的就是它 |
下图里 object5、object6、object7 就是不可达对象,该被回收:
GC Roots 是谁
GC Roots 自己必须是可达的,从它们出发扫到的才站得住。Java 里常见四类:
-
虚拟机栈(栈帧里的本地变量表)引用的对象 -
方法区中静态属性引用的对象 -
方法区中常量引用的对象 -
本地方法栈中 JNI 引用的对象
这几类和 JVM 内存结构绑得很紧,这里先记住:每次 GC 都从这些根开始,把能走到的节点标成存活。
三、有哪些方式回收垃圾
GC Roots 走不到的就是垃圾。下图黑色是垃圾,灰色是存活,绿色是空白:
| 算法 | 做法 | 适合 |
|---|---|---|
| 标记-清理 | 先按可达性标记存活/垃圾,再把垃圾空间直接清空 | 简单,但容易碎 |
| 标记-整理 | 清理时把存活对象挤到一起,空隙被挤掉 | 存活多、垃圾少 |
| 复制 | 堆分成两块,只用一块;满了把存活对象拷到另一块,当前块整段清空 | 存活少、垃圾多;可用空间只有一部分 |
标记-清理
标记完再扫一遍清空垃圾空间,结果如下:
方便,但容易留下内存碎片。
标记-整理
清理时把存活对象扎堆,碎片就少了:
标记-清理和标记-整理都更适合存活多、垃圾少:只要清掉少量垃圾,再挪一下活着的对象。
复制
做法更粗:堆一分为二,一段时间只用一块;这块用完后,把存活对象全部拷到另一块,当前块直接清空。
不容易碎,也简单;代价是同一时刻只能用一半内存,超了就要频繁复制、清空。适合存活少、垃圾多:复制过去的对象不多,大部分垃圾直接扔掉。
四、Java 的分代回收
三种算法都在,Java 不会只用一种。先看堆怎么切。
堆结构
一块 Java 堆通常分成三块,对应三类数据:
-
新生代:刚创建的对象。局部变量很快变成不可达,「朝生夕死」,特点是存活少、垃圾多。 -
老年代:活了挺久的对象,特点是存活多、垃圾少。 -
永久代:静态文件这类几乎不用回收的对象。Java 8 已去掉永久代,空间给了元空间,这里不展开。
常规堆至少有新生代和老年代。新生代用复制,老年代用标记-整理。
新生代:复制
每次 GC 都会死掉大量新对象,只有少量活着,所以用复制:把少量存活对象拷走即可。怎么切这块内存,有三种思路。
思路 1:1:1
每次只用一半;满了把存活对象拷到另一半,当前半清空。问题是可用内存只有一半,新对象又来得勤,GC 会过于频繁,影响正常运行。
思路 2:9:1
先用 9 那块,快满时把存活对象拷到 1,再清空 9。可用空间大多了,但 1 区可能装不下,只好把对象送到老年区——于是老年区里会混进「并不老」的对象,破坏了分区规则。
思路 3:8:1:1(Eden + 两个 Survivor)
依次叫 Eden、Survivor A、Survivor B。Eden 是「伊甸园」,新对象大多在这出生;Survivor 是经历过 GC 还活着的。流程是:
-
Eden 最大,对外供堆。快满时做 Minor GC,存活对象进 Survivor A,清空 Eden。 -
Eden 清空后再对外供堆。 -
Eden 再满时,对 Eden 和 Survivor A 一起 Minor GC,存活对象进 Survivor B,两边都清空。 -
之后重复:Eden 满了,就把 Eden 和某一个 Survivor 的存活对象拷到另一个 Survivor。 -
某个 Survivor 装满还拷不完,或对象反复 Survive 约 15 次,剩余对象进 Old。 -
Old 也满了,做 Major GC,回收老年代。
真实 JVM 里可用 SurvivorRatio 配 Eden 和单个 Survivor 的比例,默认是 8。
老年代:标记-整理
老年代里多是活得久的对象,每次 GC 只有少部分被收掉,属于存活多、垃圾少,所以用标记-整理:挪一点点对象就能清垃圾,也不碎。
小结:新生代用复制,老年代用标记-整理。
五、小结与参考
垃圾回收是 Java 的基本功,也是往高级工程师走必须啃的一块。
参考文章:
原文出处:wingjay
一句话总结:从 GC Roots 走不到的就是垃圾。新生代垃圾多,用复制(Eden + 两个 Survivor);老年代活得久,用标记-整理。分代,就是按对象寿命选不同的扫帚。
转载请注明来源:Java 技术之垃圾回收机制
















