JVM 的垃圾回收机制是 Java 实现自动内存管理的核心,它的主要作用是自动识别并回收不再被使用的对象所占用的内存,以此避免内存泄漏和内存碎片问题,确保程序能高效稳定地运行。
确定垃圾对象
要进行垃圾回收,首先得明确哪些对象是垃圾对象。JVM 采用了两种常见的算法来判定:
- 引用计数算法::给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加 1;当引用失效时,计数器值减 1。当计数器为 0 时,就认为该对象是垃圾对象。不过这个算法存在循环引用的问题,即两个对象相互引用,它们的计数器都不为 0,但实际上它们都不再被其他对象引用,这样就无法被回收。
- 可达性分析算法:从一系列被称为 “GC Roots” 的对象开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的,会被判定为垃圾对象。常见的 GC Roots 包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象以及本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
垃圾回收算法
确定了垃圾对象后,JVM 会采用不同的垃圾回收算法来回收这些对象:
- 标记 - 清除算法:先标记出所有需要回收的对象,之后统一回收被标记的对象。这种算法实现简单,但缺点是会产生大量内存碎片,可能导致后续无法为大对象分配连续的内存空间。
- 复制算法:将内存划分为大小相等的两块,每次只使用其中一块。当这块内存用完后,把存活的对象复制到另一块内存上,然后将已使用过的内存空间一次性清理掉。该算法不会产生内存碎片,但内存利用率较低,因为总有一半的内存处于闲置状态。
- 标记 - 整理算法:先标记出需要回收的对象,接着将存活的对象向内存的一端移动,最后清理掉边界以外的内存。它解决了标记 - 清除算法的内存碎片问题,不过移动对象会带来一定的性能开销。
- 分代收集算法:结合了上述几种算法的优点,根据对象的存活周期将内存划分为不同的区域。一般分为新生代和老年代。新生代中的对象存活时间短,采用复制算法;老年代中的对象存活时间长,采用标记 - 清除或标记 - 整理算法。
垃圾回收器
不同的垃圾回收器实现了上述的垃圾回收算法,JVM 提供了多种垃圾回收器,以满足不同场景的需求:
- Serial 回收器::是一个单线程的回收器,在进行垃圾回收时会暂停所有的用户线程,适用于单 CPU 环境下的小型应用。
- Parallel 回收器:是 Serial 回收器的多线程版本,多个线程同时进行垃圾回收,提高了回收效率,适合对吞吐量要求较高的应用。
- CMS 回收器:取最短回收停顿时间为目标,采用标记 - 清除算法,在垃圾回收过程中尽可能减少对用户线程的影响,但会产生内存碎片,并且对 CPU 资源比较敏感。
- G1 回收器:将整个堆划分为多个大小相等的 Region,采用标记 - 整理算法,可预测垃圾回收停顿时间,能同时兼顾吞吐量和低延迟,适用于大内存、多 CPU 的服务器环境。
垃圾回收的触发时机
JVM 的垃圾回收分为新生代垃圾回收(Minor GC)和老年代垃圾回收(Major GC/Full GC)
- Minor GC:当新生代的 Eden 区满时,会触发 Minor GC,对新生代进行垃圾回收。由于新生代中大部分对象的存活时间较短,所以 Minor GC 的频率较高,但回收速度也较快。
- Major GC/Full GC:当老年代空间不足时,会触发 Major GC 或 Full GC,对老年代和整个堆进行垃圾回收。触发的原因可能是新生代晋升到老年代的对象大小超过了老年代的剩余空间,或者永久代(在 JDK 8 之前)空间不足等。Full GC 的速度通常比 Minor GC 慢很多,因为它涉及的内存区域更大,并且可能会进行更多的对象移动和整理操作。