(二)GC 回收(垃圾回收)机制与分代回收策略

767 阅读6分钟
什么是垃圾

垃圾就是内存中已经没有用的对象 Java 虚拟机中使用"可达性分析”的算法来决定对象是否为垃圾


可达性分析

a9342308be0eff45ec019ba654a74465.png

注意:上图中的图标实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。

GC Root 对象
  1. Java 虚拟机栈(局部变量表)中的引用的对象
  2. 方法区中静态引用指向的对象
  3. 仍处于存活状态中的线程对象
  4. Native 方法中 JNI 引用的对象

什么时候回收

不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。

1.Allocation Failure:

在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

2.System.gc():

在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。


如何回收垃圾
标记清除算法(Mark and Sweep GC)

从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收

  1. Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。

  2. Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。

35d6284f12b6c3ea79f128bae7a3def0.png

  • 优点:实现简单,不需要将对象进行移动。
  • 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
复制算法(Copying)

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  1. 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A ede3e1cb6f420cff783f44d821254957.png
  2. 标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存 32728ef967ab5863bb4120883923863e.png
  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
标记-压缩算法 (Mark-Compact)

从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。

1.Mark标记阶段:找到内存中的所有GCRoot对象,只要是和GCRoot对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)

2.Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。

4e497df878a8183ad7491ae547baeabc.png

  • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

  • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。


JVM分代回收策略

Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代回收策略。 注意: 在 HotSpot 中除了新生代和老年代,还有永久代。

  • 对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将它们转移到老年代中。
年轻代(Young Generation)
  1. 新生成的对象优先存放在新生代中
  2. 存活率很低,回收效率很高
  3. 一般采用的 GC 回收算法是复制算法

新生代又可以继续细分为 3 部分: Eden Survivor0(简称 S0) Survivor1(简称S1) 这 3 部分按照 8:1:1 的比例来划分新生代

  • 内存分配过程如下:
  1. 大多数刚刚被创建的对象会存放在 Eden区

c492a8f415d079d8e9eee99ac3feb4c4.png

  1. 当 Eden 区第一次满的时候,会进行垃圾回收。首先将 Eden 区的垃圾对象回收清除,并将存活的对象复制到 S0,此时 S1 是空的

24e334518d51e29119e5859da2acb8f6.png

  1. 下一次 Eden 区满时,再执行一次垃圾回收。此次会将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1,此时 S0 变为空

ec51b38d126c5afafa46ebd79d2dba7d.png

  1. 反复在 S0 和 S1之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中

09df91fde63f6fa2c92663f7f25e319c.png

老年代(Old Generation)
  1. 老年代能存放更多的对象
  2. 老年代因为对象的生明周期较长
  3. 一般采用标记压缩的回收算法

在新生代存活了足够长的时间而没有被清理掉则复制到老年代 如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上

  • 注意:老年代中的对象有时候会引用到新生代对象。如果要执行新生代GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个512 byte 的 card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个 card table 即可,大大提高了性能。

GC Log

066413d2a5b1a8d6de8d8fc08d968ebd.png

8927f61416e7da9306f1d5eadd33964d.png


引用

346126658a9dd3c69ccffe65f5c4c724.png

平时项目中,尤其是Android项目,因为有大量的图像(Bitmap)对象,使用软引用的场景较多


总结

可达性分析

JVM 中使用可达性分析来判断对象是否可以被回收

几种回收算法

标记清除算法(Mark and Sweep GC) 复制算法(Copying) 标记-压缩算法 (Mark-Compact)

分代回收策略

新生代 (Eden、Survivor0、Survivor1) 老年代

虚拟机垃圾回收机制很多时候都是影响系统性能、并发能力的主要因素之一。尤其是对于从事 Android 开发的工程师来说,有时候垃圾回收会很大程度上影响 UI 线程,并造成界面卡顿现象