目录
Parallel Scavenge收集器 / Parallel Old收集器
1、java内存区域
Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而 Java 堆不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
2、判断哪些对象需要被回收
有以下两种方法:
- 引用计数法
给对象添加一引用计数器,被引用一次计数器值就加 1;当引用失效时,计数器值就减 1;计数器为 0 时,对象就是不可能再被使用的,简单高效,缺点是无法解决对象之间相互循环引用的问题。 - 可达性分析算法
通过一系列的称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。此算法解决了上述循环引用的问题。
在Java语言中,可作为 GC Roots 的对象包括下面几种:
a. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
b. 方法区中类静态属性引用的对象。
c. 方法区中常量引用的对象。
d. 本地方法栈中 JNI(Native方法)引用的对象
3、垃圾收集算法
一共有 4 种:
- 标记-清除算法
- 复制算法
- 标记整理算法
- 分代收集算法
标记-清除算法
最基础的收集算法是“标记-清除”(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:
- 效率问题,标记和清除两个过程的效率都不高;
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记—清除算法的执行过程如下图。
复制算法
为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。复制算法的执行过程如下图:
标记-整理算法
复制算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如下:
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不用的垃圾收集算法。
4、垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。这里讨论的收集器基于JDK 1.7 Update 14之后的 HotSpot 虚拟机,这个虚拟机包含的所有收集器如下图所示
上图展示了 7 种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
UseConcMarkSweepGC收集器
CMS、G1 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)。
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉1分钟,那吞吐量就是99% 。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
优点:
低停顿时间:CMS收集器通过在垃圾回收过程中与应用程序并发执行,尽量减少了应用程序的停顿时间。它使用了并发标记和并发清除的算法,在大部分时间内与应用程序并发工作,从而减少了垃圾回收对应用程序的影响,适用于对停顿时间要求较高的应用场景。
垃圾回收线程与应用程序线程并发执行:CMS收集器使用多个垃圾回收线程与应用程序线程并发执行,减少了垃圾回收线程与应用程序线程的竞争,提高了垃圾回收的效率和吞吐量。
缺点:
碎片问题:CMS收集器使用了并发清除算法,清除过程中会产生内存碎片。这可能导致当应用程序需要分配大对象时,无法找到足够连续的内存空间,进而触发Full GC(全局垃圾回收),导致较长的停顿时间。因此出现了G1收集器。
G1收集器(-XX:+UseG1GC 使用 G1垃圾收集器)
JVM中有一个参数-XX:+PrintFlagsFinal,可以打印出几乎所有的JVM支持的参数以及他们的默认值
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,G1 是一款面向服务端应用的垃圾收集器。HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。与其他 GC 收集器相比,G1 具备如下特点。
并行与并发: G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
分代收集: 与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
空间整合: 与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC 。
可预测的停顿: 这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局就与其他收集器有很大差别,它将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分 Region (不需要连续)的集合。
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1 在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是Garbage-First名称的来由),保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。