JVM垃圾回收-算法(一)

什么是垃圾?

  • 垃圾指的是不再被引用类型所引用的对象实体

为什么需要 GC

  • 如果不及时清理垃圾, 这些垃圾将会一直占用空间直到程序结束. 这样程序运行时间一长就会空间不足, 导致内存溢出

垃圾回收算法

标记阶段:

标记阶段 - 引用计数算法(Reference Counting):

在这里插入图片描述

  • 引用计数算法判断对象是否存活的方式是给每个对象分配一个整型的引用计数器属性, 用于记录对象被引用次数, 当引用计数器的值为0时表示, 可进行回收
  • 优点: 实现简单, 执行效率高, 回收没有延迟性
  • 缺点: 1. 增加了计数字段空间开销 2. 计数字段加减操作的时间开销 3. 无法处理循环引用(也就是根被删时无法识别), 导致内存泄漏
    * 由于此算法无法处理循环引用情况, 因此 JVM中没有使用引用计数算法
    * 注: JVM的垃圾回收没有采用引用计数算法

标记阶段 - 可达性分析算法(Reachability Analysis)或又称追踪性垃圾收集(Tracing Garbage Collection)/根搜索算法(GC Roots Tracing):

在这里插入图片描述

* 区别与引用计数算法, 可达性分析算法不仅同样具备实现简单和执行高效等优点, 还有更重要的是该算法有效的解决了循环引用的问题(且 JVM中使用了该算法)

  • 对象是否存活的判断条件:
  1. 可达性分析算法是, 通过 GC Roots为起始点, 按照从上至下的方式, 搜索目标对象是否可达
  2. 使用可达性分析算法后, 内存中的存活对象都会被 GC Roots直接或间接的连接着, 搜索所走过的路径称之为引用链(Reference Chain)
  3. 如果目标对象没有任何引用链相连, 则是不可达的, 意味着该对象已经死亡, 也就是允许被回收
  4. 如果能够被 GC Roots直接或间接连接着, 意味着该对象是存活对象

* 哪些对象会成为 GC Roots:

  1. 虚拟机栈内的引用类型 如方法的形参, 局部变量等
  2. 本地方法栈内的引用类型
  3. 类静态属性(Jdk6方法区/Jdk7堆空间)的引用类型
  4. 常量引用类型 如字符串常量池(String Table)的引用
  5. 所有被同步锁 synchronized持有的对象
  6. 虚拟机内部的引用: 基本数据类型对应的 Class对象, 一些常驻的异常对象 如 NullPointerException, OutOfMemoryError, 系统类加载器等
  7. 反映java虚拟机内部情况的JMX Bean, JVMTI中注册的回调, 本地代码缓存等
  • 除以上 GC Roots以外, 按不同的收集器以及回收的内存区域的不同, 还会存在’临时性’的对象
    * 当使用达性分析算法进行判断内存是否可以回收时, 用户线程会引发 STW(Stop The World), 直到垃圾回收结束, 用户线程才会恢复运行. 即使号称(几乎)不会发生停顿的 CMS收集器中枚举根节点时也会停顿

清除阶段(回收阶段):

* 通过可达性分析算法, 将存活对象标记后, 接下来执行垃圾回收(清除)

清除阶段 - 标记-清除算法(Mark-Sweep):

在这里插入图片描述

* 当堆空间内存不足时, 首先会触发 STW(Stop The World), 然后做两项工作:

  1. 标记: collector从根节点开始遍历, 将所有被引用的对象 Header中记录为可达对象
  2. 清除: collector对堆内存进行线性的遍历, 将对象 Header中未标记为可达对象的引用地址, 一律回收
  • 优点: 逻辑简单
  • 缺点:
  1. 两次全局遍历, 效率不算高
  2. GC时 STW, 导致程序的吞吐量降低, 用户体验差
  3. GC后, 可用的内存地址不连续, 所以需要额外维护一个空闲列表来记录这些内存地址
    * 何为回收? 垃圾对象实体保留不动, 只是将对象的内存地址记录到空闲列表中, 等使用时覆盖已有(垃圾)对象

清除阶段 - 复制算法(Copying):

在这里插入图片描述

* 将可用内存空间分为两块, 每次只使用其中一块, 在垃圾回收时将正在使用的内存中的所有存活对象复制(同时其内存地址具有连续性)到未被使用的内存块中, 交换两块内存的角色(from/to)完成垃圾回收

  • 优点:
  1. 没有标记和清除步骤, 实现简单, 运行高效
  2. 复制过去后保证内存空间的连续性, 不会产生内存碎片
  • 缺点:
  1. 需要两倍的内存空间, 容量上限被减半
  2. 对于 G1这种分拆成大量 Region的 GC, 复制而不是移动, 意味着 GC需要维护 Region之间对象引用关系, 不管是内存占用或者时间开销也不小
    * 适合使用复制算法的场景是, 每次 GC时存活的对象少, 垃圾较多的场景. 典型的场景就是新年代的幸存者区

清除阶段 - 标记-压缩算法(Mark-Compact, 标记整理):

在这里插入图片描述

* 复制算法的高效性是建立在存活对象少, 垃圾对象多的前提下的. 这种情况在新生代经常发生, 但是在老年代, 更常见的情况是大部分对象都是存活对象. 如果依然使用复制算法, 由于存活对象较多, 复制的成本也将提高
* 标记-清除算法也可以用在老年代中, 但是它有产生内存碎片的问题, 所以 JVM设计者在此基础上进行了改进. 由此诞生了, 标记-压缩算法

  • 执行过程:
    * 与标记-清除算法一样, 从根节点开始标记所有被引用的对象, 然后将所有的存活对象整理到内存的一侧, 按顺序排放
  • 优点:
  1. 因没有内存区域分散的问题, 给新的对象分配内存时, 只需要内存起始地址即可
  • 缺点:
  1. 移动对象时, 如果被移动的对象有被其它对象引用, 则需要一起调整引用的地址
  2. GC时 STW, 导致程序的吞吐量降低, 用户体验差

分代收集算法(Generational Collection)

  • 分代收集算法是基于对象的不同生命周期采取了不同的收集方式, 以此提高了 GC性能. 目前 JVM堆空间的新生代和老年代就是它的落地实现

增量收集算法(Incremental Collecting)

  • 上述现有的算法, 在垃圾回收过程中, 用户线程会处于 STW(Stop The World), 直到垃圾回收完成, 因此影响用户体验. 为解决这个问题出的就是增量收集算法
  • 设计思想: 如果一次性将所有的垃圾进行处理, 会造成系统长时间的停顿, 那么就可以让垃圾收集线程和用户线程交替执行(就是并发行为). 每次收集一小片区域的内存空间, 接着切换到用户线程. 依次反复, 直到垃圾收集完成
    * 增量收集算法的基础仍是传统的标记-清除和复制算法. 增量收集算法通过对线程间冲突的妥善处理, 允许垃圾收集线程以分阶段的方式完成标记, 清理或复制工作

缺点: 垃圾回收线程和用户线程之间频繁切换上下文, 使得垃圾回收的总体成本上升, 造成系统吞吐量的下降

分区算法

  • GC时 相同条件下, 堆空间越大, 用户线程会处于 STW的时间就越长. 为了更好地控制 GC产生的停顿时间, 将一块大的内存区域分割成多个小块, 根据目标的停顿时间, 每次合理的回收若干个小区间, 而不是整个堆空间, 从而减少一次 GC所产生的停顿

如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

猜你喜欢

转载自blog.csdn.net/qcl108/article/details/108816768
今日推荐