文章目录
- JVM 的内存模型
- 对象存活?
- 引用计数算法
- 可达性分析算法
- 垃圾收集
- 标记-清除算法
- 标记-复制算法
- 标记-整理算法
- 垃圾收集器
- 垃圾收集器发展
- Serial / Serial Old
- Parallel Scavenge / Parallel Old
- ParNew / CMS
- G1
- ZGC
- 扩展
JVM 的内存模型
Java 虚拟机(Java Virtual Machine,简称 JVM)根据《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域,如图所示:
对于 Java 应用程序来说,其中 Java 堆(Java Heap)和方法区(元空间或者永久代)是虚拟机所管理的内存中最大的一块。而垃圾回收机制所关注的正是这部分内存该如何管理。
其中方法区垃圾收集的“性价比”通常也是比较低的;在 Java 堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收 70%至 99%的内存空间,相比之下,方法区的垃圾收集的回收成果往往远低于此。同时《Java 虚 拟机规范》中提到过可以不要求虚拟机在方法区中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK 11 中的 ZGC 收集器就不支持类卸载)。
在 Java 中,垃圾回收机制(Garbage Collection,简称 GC)是一个非常重要的概念,它主要的作用是回收程序当中不再使用的内存。因此 GC 要负责完成 3 项任务:分配内存,确保被引用对象的内存不被错误地回收,回收不再被引用的对象的内存。
对象存活?
在 Java 堆里面存放着几乎所有的对象实例,GC 在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了,对于“死去”的对象我们会将其标记为垃圾,之后 GC 会将这些进行标记的进行回收。判断对象是否“存活”的算法主要为两种:引用计数算法,可达性分析算法。
引用计数算法
引用计数法通过在堆中对每个对象都有一个引用计数器;当对象被引用时,计数器值就加 1;当引用被置空或者离开作用域时,计数器值就减 1:任何时刻计数器为零的对象就是不可能再被使用的。
引用计数算法虽然简单,但是如果出现两个对象之间互相引用时,两个对象都是垃圾,但是计数器值永远不为 0,GC 就无法对其回收。因此 Java 使用了可达性分析算法来判定对象是否存活的。
可达性分析算法
可达性分析算法就是通过一系列称为“GC Roots’的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象不可达时,意味着此对象是不可能再被使用的,就被判定为垃圾。
Java 中固定可作为 GC Roots 的对象包括以下几种:
- 栈中引用的对象;
- 类静态属性引用的对象;
- 常量引用的对象;
- Native 本地方法中引用的对象;
垃圾收集
首先,基于弱分代假说和强分代假说形成了分代收集理论:
- 弱分代假说(Weak Generational Hy pothesis):绝大多数对象都是朝生夕灭的。
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
根据分代收集理论一般会把 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。针对 GC 对回收其中某一个或者某些部分的区域,又划分为“Minor GC"Major GC"和“Full GC 这样的回收类型
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。由于一般 Major GC 发生的时候通常也会伴随着 Minor GC,所以 Major GC 也经常被称为 Full GC 或 FGC,也就是全局范围的 GC 动作。
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
标记-清除算法
“标记-清除”(Mark-Sweep)算法分为“标记和“清除两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
当垃圾收集器将内存扫描之后会标记出所有垃圾对象然后将它们回收,但这个方法有一个缺点,就是会不断的会产生大量不连续的内存碎片,使内存的使用率变得越来越低,也可能会导致以后程序无法为大对象分配一片连续的内存空间。
标记-复制算法
标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当第一块的内存剩余不足时,会将所有需要保留的对象复制到另外一块内存,然后再把已使用过的内存直接清空。
标记-复制算法既做到了垃圾回收,又做到了碎片整理,但代价是将可用内存缩小为了原来的一半,内存的空间浪费一倍。
标记-整理算法
标记-整理(Mak-Compact)算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。简单来说就是在清理垃圾的基础上,增加了一步碎片整理的操作。
垃圾收集器
垃圾收集器发展
垃圾收集器根据迭代版本分为:
- 早期的 Serial 和 Serial Old;
- 中期的 Parallel Scavenge 和 Parallel Old;
- 过渡期的 ParNew 和 CMS;
- JDK9 默认垃圾收集器 G1(Garbage First);
- JDK11 中新加入的低延迟垃圾收集器 ZGC(Z Garbage Collector)。
Serial / Serial Old
Serial 是一个工作在新生代的单线程收集器,与它搭档负责老年代的垃圾收集器为 Serial Old。单线程意味着在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束,这个动作习惯称为 STW(Stop The World)。Serial/Serial Old 收集器的运行过程如图:
Parallel Scavenge / Parallel Old
Parallel Scavenge 是多线程的收集器,与它搭档的是 Parallel Old。不同于 Serial/Serial Old 收集器的是,当开始垃圾回收时,所有用户线程必须全部暂停,依然触发了 STW,不过这次垃圾回收变成了多线程,对于多 CPU 的服务器来讲,提高了不少效率,不过依然无法避免 STW。
ParNew / CMS
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,与它搭档工作在新生代的垃圾收集器为 ParNew,与 Parallel Scavenge 基本相同。CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个阶段,括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清理(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然会触发 STW。初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快;并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间的错标问题,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的己经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。CMS 的并发工作流程如下:
G1
G1 是一款主要面向服务端应用的垃圾收集器。JDK9 中,G1 取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的默认垃圾收集器。G1 允许用户手动设置一个期望的 STW 时间。
G1 基于 Region 的堆内存布局,即 G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region,Region 的大小可以通过参数-XX:G1Heap RegionSize
设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂),每一个 Region 都可以根据需要,扮演新生代的 Ede 空间、Survivor 空间,或者老年代空间,而新生代和老年代的空间大小不再是绝对固定,而且当 GC 扫描内存时,无需扫描整块内存,只需扫描特定区域即可,极大的提高了它所能支持的堆内存的大小
Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。G1 认为只要大小超过了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize
设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,直至被回收之前,在内存的位置始终保持不变,避免了对老年代整理时频繁的移动大对象。
ZGC
ZGC 是一款在 JDK 11 中新加入的具有实验性质的低延迟垃圾收集器,在 JDK 15 中正式投入生产使用了,使用 –XX:+UseZGC
命令可以启用 ZGC。
ZGC 与 G1 一样,也采用了基于 Region 的堆内存布局,但与不同的是,ZGC 没有分代的概念,而且 Region 具有动态性——动态创建和销毁,以及区域容量大小也是动态的。
在 x64 硬件平台下,ZGC 的 Region 可以具有大、中、小三类容量:
- 小型 Region(Small Region):容量固定为 2MB,用于放置小于 256KB 的小对象。
- 中型 Region(Medium Region):容量固定为 32MB,用于放置大于等于 256KB 但小于 4MB 的对象。
- 大型 Region(Large Region):容量不固定,可以动态变化,但必须为 2MB 的整数倍,用于放置 4MB 或以上的大对象。每个大型 Region 中只会存放一个大对象,这也预示着虽然名字叫作“大型 Region”,但它的实际容量完全有可能小于中型 Region,最小容量可低至 4MB。
ZGC 运作过程如图:
ZGC 做到了几乎整个收集过程都全程可并发,短暂停顿也只与 GC Roots 大小相关而与堆内存大小无关,因而实现了任何堆上停顿都小于十毫秒的目标。
扩展
在命令行中使用以下命令查看 JDK 使用的垃圾收集器:
java -XX:+PrintCommandLineFlags -version