Java 技术之垃圾回收机制

    |     2017年10月10日   |   Java技术   |     0 条评论   |    1136

垃圾回收是 Java 非常重要的特性之一,也是面试常客。它让开发者不必自己申请、释放空间,而是由后台守护进程在合适的时候把垃圾清掉——既省事,也更不容易把堆撑爆。

下面按四个问题把机制拆开:什么是堆、什么是垃圾、怎么回收、为什么要分代。

20140731144525904006

一、什么是 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 技术之垃圾回收机制
本文链接地址:https://ai.zhousir.top/?p=2163
回复 取消