垃圾收集算法和常用的垃圾收集器学习记录

241 阅读7分钟

垃圾收集概述


垃圾收集需要关注三个问题 :

  • 哪些内存需要回收
  • 何时被回收
  • 如何回收

由于程序计数器和虚拟机栈都是线程私有的,随线程而生,随线程而灭,不需要我们关注。

堆和方法区有着很大的不确定性,我们所关注的正是这部分内存区域。


哪些对象可以被回收


可达性分析算法

当前主流的虚拟机,都是通过可达性分析算法判断对象是否可以被回收。

通过一系列称为 "GC Roots" 的根对象作为起始节点集 ,从这些节点开始,根据引用关系向下搜索,如果某个对象到 GC Roots 间没有任何引用链相连,则这个对象可以被回收

图中 object5、object6、object7 可以判定被回收。

哪些对象是 GC Roots 呢:

  • 被同步锁(synchronized 关键字)持有的对象。
  • Native 方法中引用的对象。
  • 方法区中的类静态属性引用的对象,例如 private staic Object objectA = new Object()objectA 就是 GC Roots。
  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象。

方法区回收

方法区回收主要包括废弃的常量和不再使用的类

判断常量是否可回收: 没有任何引用指向这个常量。

判断类是否可以被回收:

  • 类的类加载器已经被回收 (很难达成)
  • 该类所有的实例都已经被回收,堆中不存在该类及其任何派生子类的实例。

垃圾收集算法


分代收集理论

当代垃圾收集器,大多数遵循分代收集理论,分代收集理论建立在两个分代假说之上:

  • 弱分代假说: 绝大多数对象都是朝生夕灭的
  • 强分代假说: 熬过越多次垃圾收集过程的对象就越难以消亡

两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将java堆划分为不同的区域,然后将回收对象依据其年龄(熬过垃圾回收器的次数)分配到不同的区域中存储


根据分代收集理论,可以将 Java 堆划分为 新生代(Young Generation)老年代(Old Generation) 两个区域。

  • 新生代 : 每次垃圾收集时都会有大批对象死去,熬过一定回收次数的对象,会晋升到老年代中。

根据分代收集理论,可以将垃圾回收划分为不同的类型:

  • Minor Gc / Young GC :目标只是新生代的垃圾收集。
  • Major Gc / Old Gc : 目标只是老年代的垃圾收集。
  • Mixed Gc : 同时收集新生代和老年代,目前只有G1收集器有这种行为。
  • Full Gc : 目标是整个java堆和方法区的收集。

标记-清除算法

标记出所有需要收集的对象,然后清除。这是最基础的垃圾收集算法,后面的算法都是根据该算法改进的。

缺点:

  • 标记、清除之后会产生大量不连续的内存碎片,分配大内存对象时很可能找不到足够的连续内存。

标记-复制算法

将内存划分为等大小的两块,每次只使用一块,当发生回收时,把存活对象复制到另一块区域,再把已使用的内存空间一次性清理掉。

优点:

  • 解决了内存碎片化问题

缺点:

  • 将可用内存缩小为原来的一半,浪费太多


更优化的半区分配策略

由于新生代 90% 以上的对象都熬不过第一轮回收,所以不需要 1:1 划分内存空间。

把新生代划分为一块较大的 Eden 区和两块较小的 Survivor 区,每次分配内存只使用 Eden 区和一块 Survivor 区,发生垃圾收集时,将 Eden 区和使用的 Survivor 区的存活对象复制到另一块 Survivor 区,然后回收 Eden 区和那块 Survivor 区。

HotSpot 虚拟机默认 Eden 区和 Survivor 区大小为 8:1 ,即每次新生代中可用内存空间为整个新生代容量的90%。


标记-整理算法

标记整理算法的标记过程与标记清除算法类似,不过后续步骤是让所有存活对象都向内存空间一端移动。

优点:

  • 解决了内存碎片化问题

缺点:

  • 在老年代中,每次回收后仍有大量对象存活,复制过程开销很大。

垃圾收集器

收集算法是内存回收的方法论,垃圾收集器则是内存回收的实践者。

垃圾收集器从特点上可以分成:

  • 串行垃圾收集器(SerialSerial Old)
  • 并行垃圾收集器 (Parallel)
  • 吞吐量优先的垃圾收集器 (Parallel ScavengeParallel Old)
  • 响应时间优先的垃圾收集器(CMS

图中收集器所处的区域,代表他们是属于新生代收集器还是老年代收集器,如何两个收集器存在连线,代表他们可以搭配使用。


Serial收集器

Serial 收集器是最基础、历史最悠久的收集器。Serial 收集器是一个单线程工作的收集器,这里的单线程不仅指它只会用一条线程处理垃圾,也是强调它在进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)。它作用于新生代,采用 标记-复制 算法。

虽然 "Stop The World" 可能让用户体验很差,但是 Serial 收集器简单而高效,是所有垃圾收集器中额外内存消耗最小的。它是 HotSpot 虚拟机运行在客户端模式下的默认新生代收集器。


Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,也是单线程处理垃圾,采用 标记-整理 算法,也会 "Stop The World"。


ParNew 收集器

ParNew 收集器实际上是 Serial 收集器的多线程并行版本,它使用多条线程进行垃圾处理,除此之外,它和 Serial 收集器几乎没有区别,也会 "Stop The World",,作用于新生代,采用标记-复制算法。


Parallel Scavenge

Parallel ScavengeParNew 收集器一样,但它更关注吞吐量

-XX:+UseAdaptiveSizePolicy 参数能动态的调整虚拟机参数,以提供最大的吞吐量。


Parallel Old 收集器

Parallel OldParallel Scavenge 收集器的老年代版本,支持多线程并行收集,基于标记-整理算法。

在注重吞吐量的场合,优先考虑 Parallel Scavenge + Parallel Old 收集器。


CMS 收集器

CMS(Concurrent Mark Sweep) 是以 响应时间优先(即每次 Stop The World 的时间最短) 为目标的垃圾收集器。基于标记-清除算法,整个过程分为四步:

  • 初始标记 (Stop The World)
  • 并发标记,与用户线程同时运行
  • 重新标记 (Stop The World)
  • 并发清除,与用户线程同时运行

初始标记是标记直接和 GC Roots 关联的对象。

并发标记是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程不需要停顿用户线程。

重新标记是修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

CMS 收集器的特点:并发收集、低停顿。

缺点:

  • 采用标记-清除算法,会导致内存碎片化问题。
  • 无法处理 "浮动垃圾",在并发清除节点,用户线程产生的垃圾,必须等到下次 GC 时才能回收,这部分垃圾称为 "浮动垃圾"。
  • 在并发阶段,占用了一部分线程,可能导致程序吞吐量降低。

G1 收集器

JDK 9以后,服务端下的默认垃圾收集器就是 G1G1 面向的是整个堆内存(不仅仅局限于新生代或者老年代)。

G1 把整个 Java 堆划分为多个大小相等的独立区域(称为 Region),每一个 Region 都可以 根据需要,扮演新生代的 Eden 空间、 Survivor 空间,或者老年代空间。

G1Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region大小的整数倍。

G1 回收分为四个阶段:

  • 初始标记 (Stop The World)
  • 并发标记,与用户线程同时运行
  • 最终标记 (Stop The World)
  • 筛选回收 (Stop The World)

G1 的优势:

  • G1 总体是基于标记整理算法,局部区域基于标记复制算法,不会存在内存空间碎片化问题
  • G1 同时追求吞吐量和低延迟