垃圾回收算法的演进包括标记和清除两个主要阶段。初始阶段采用标记清除算法,通过标记存活对象,再清除未标记的垃圾对象。为了解决标记清除算法的空间碎片问题,引入了标记整理算法,它在标记的基础上将存活对象整理到内存的一端,减少碎片。复制清除算法则将内存划分为两个区域,通过复制存活对象至一块区域,再清除未复制的区域,解决了碎片问题。
随着对象生命周期的不同,引入了分代垃圾回收算法,标记阶段采用可达性分析, 清除阶段将堆内存划分为新生代和老年代,根据不同特性分别采用适合的回收算法。在标记阶段,这些算法都依赖于可达性分析,通过根对象集合追踪引用链,确定存活对象。
在 JDK 1.8 中,为提高效率和适应多核处理器,引入了并行收集器,采用多线程并行处理垃圾回收。同时,G1 收集器作为一种新型收集器,引入了分区概念,以更灵活、高效地管理内存,逐步替代了 CMS(Concurrent Mark-Sweep) 收集器。这些演进的算法和技术共同构成了现代 Java 虚拟机垃圾回收的体系。
在Java虚拟机(JVM)中,堆是存放几乎所有Java对象实例的地方。在执行垃圾回收之前,首要任务是区分内存中的存活对象和已经死亡的对象。只有被标记为已经死亡的对象,垃圾回收才会在执行时释放其占用的内存空间,这个过程通常称为垃圾标记阶段。
那么在JVM中,是如何判断一个对象是否死亡的呢?简而言之,当一个对象不再被任何存活对象引用时,就可以被判定为已经死亡。
引用计数算法(Reference Counting)相对简单,为每个对象维护一个整型的引用计数器属性,用于记录对象被引用的情况。
对于一个对象A,只要有任何一个对象引用了A,A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:
实现简单,垃圾对象容易辨识;
判定效率高,回收没有延迟性。
缺点:
需要单独的字段存储计数器,增加了存储空间的开销;
每次赋值都需要更新计数器,伴随着加法和减法操作,增加了时间开销;
无法处理循环引用的情况,是引用计数算法的致命缺陷,会出现内存泄露问题。
由于引用计数算法的缺陷,Java的垃圾回收器并未采用这种算法,而是选择了更为可靠的可达性分析算法。可达性分析通过一系列的根对象出发,追踪对象之间的引用关系,判断对象的存活状态,从而决定是否进行回收。
概念:
可达性分析算法,也称为根搜索算法或追踪性垃圾收集,是一种用于判断对象是否存活的垃圾回收算法。相对于引用计数算法,可达性分析算法不仅实现简单且执行高效,更重要的是能够有效解决引用计数算法中可能发生的循环引用导致的内存泄漏问题。
思路:
GC Roots集合(根集合): 可达性分析算法以GC Roots集合为起始点,GC Roots是一组必须保持活跃的引用。这些引用可以是虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象,以及本地方法栈中JNI(Java Native Interface)引用的对象。
搜索可达对象: 从GC Roots集合出发,按照从上至下的方式搜索被GC Roots集合所连接的目标对象是否可达。这个搜索的过程构成了引用链(Reference Chain)。
标记存活对象: 使用可达性分析算法,内存中的存活对象都会被GC Roots集合直接或间接连接。如果一个对象能够通过引用链与GC Roots集合相连,则被认为是存活对象。只有能够被连接到GC Roots集合的对象才是存活对象。
标记垃圾对象: 如果目标对象没有任何引用链相连,说明该对象不可达,即已经死亡,可以标记为垃圾对象。这些垃圾对象将在后续的垃圾回收阶段被清理。
可达性分析算法通过判断对象是否与GC Roots集合相连,实现了对存活对象和垃圾对象的准确区分,避免了引用计数算法中循环引用导致的内存泄漏问题。这一算法为现代垃圾回收器的实现提供了有效的基础。
GC Roots 是一组必须保持活跃的引用,它们作为起始点,帮助可达性分析算法判断对象的存活状态。以下是可能成为 GC Roots 的对象:
虚拟机栈中的引用: 对象被局部变量表中的引用变量所引用,即使是方法调用尚未完成,这些引用仍然有效。
本地方法栈中 JNI 引用的对象: JNI(Java Native Interface)是 Java 调用本地语言的接口,本地方法栈中保存了对 Java 对象的引用。
方法区中类静态属性引用的对象: 类的静态属性属于类本身,可以在方法区中被引用,即使类的实例已经被回收,其静态属性依然有效。
方法区中常量引用的对象: 常量池中的字面量、符号引用等常量,被类的方法引用,从而形成对对象的引用。
活动的线程: 正在执行的线程的栈帧中的本地变量表、操作数栈中引用的对象,以及正在被调用的方法的参数、返回值等。
JNI 引用的全局变量: 在 native 方法中使用 NewGlobalRef 方法创建的对象引用。
总的来说,GC Roots 就是一组确保引用链的起始点,使得通过引用链能够遍历到所有存活对象的集合。通过维护这组引用,垃圾收集器可以准确判断对象的存活状态,确保不会将存活对象误判为垃圾对象。
概念:
标记清除算法是一种基础的垃圾回收算法,通过标记和清除两个阶段来实现对垃圾对象的回收。
过程:
标记阶段: 从根对象开始,遍历所有可达对象,标记它们。
清除阶段: 清除未标记的对象,即被标记为垃圾的对象。
原理:
缺点:会产生内存碎片,不同大小的碎片可能导致无法分配大对象。
概念:
标记整理算法在标记清除的基础上改进,主要解决内存碎片问题。
过程:
标记阶段: 与标记清除算法相同,标记可达对象。
整理阶段: 将活动对象整理到一端,清除边界外的内存。
原理:
通过整理阶段,将存活对象集中在一起,减少了内存碎片。
概念:
复制清除算法分为标记和复制两个阶段,主要解决停顿时间较长的问题。
过程:
标记阶段: 从根对象标记可达对象。
复制阶段: 将活动对象复制到新区域,清除旧区域。
交换区域: 将新区域和旧区域交换角色。
原理:
通过复制阶段,减少了停顿时间,但需要额外的内存空间。
分代垃圾回收算法
概念:
分代垃圾回收算法基于“弱分代假设”,将堆内存分为新生代和老年代。
过程:
新生代: 使用复制清除算法,将新对象复制到新生代,并晋升到老年代。
老年代: 使用标记清除或标记整理算法,处理老年代对象。
原理:
适应对象生命周期的不同特点,新生代对象生命周期短,老年代对象生命周期长。
特点:
主要用于新生代垃圾回收。
使用多线程并行处理,提高回收效率。
适合多核服务器应用场景。
原理:
通过并行处理,提高了垃圾回收的吞吐量。
特点:
使用分区的概念,避免了全堆扫描。
针对大内存、多核处理器环境做了优化。
逐步替代 CMS 收集器。
原理:
通过分区和优化算法,降低了停顿时间,适用于大内存应用。