graph TD
垃圾回收 --> 垃圾收集区域
垃圾回收 --> 判断对象已死
垃圾回收 --> 垃圾收集算法
垃圾回收 --> 垃圾收集类型
垃圾回收 --> 垃圾收集器
垃圾收集区域
JVM 的运行时数据区中,程序计数器、虚拟机栈和本地方法栈随线程生命周期变化,内存分配与回收具有确定性,无需特殊垃圾回收处理。比如虚拟机栈中的栈帧大小通常在编译期就可以确定,随着方法调用的执行,栈帧按需入栈和出栈,内存的分配和回收是自动并且高效的。
而 Java 堆和方法区的内存需求在运行时动态决定,垃圾收集器主要针对这两个区域进行工作, 回收不再使用的对象和无用的类加载信息等,以管理内存资源并防止内存泄漏。
判定对象可收集
graph LR
root["GC Root"]
判定对象可回收 --> 引用计数法
判定对象可回收 --> 可达性分析
可达性分析 --> root
可达性分析 --> 对象引用
- 引用计数法:存在循环引用问题,使用它该方法的性能开销和解决缺陷的复杂度代价太大。
- 可达性分析法(Java使用):从一系列可作为 GC Root 的根节点开始搜索,凡是在引用链上的对象,就不会被垃圾回收。
可以作为 GC Root 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
Java对象的引用
- 强引用:直接赋值或 new 创建的对象引用,只要强引用存在,对象绝不会被回收;
- 软引用:使用 SoftReference 类实现,代表有用但非必要的对象。当内存不足时,会在发生OOM 前尝试回收这些对象;
- 弱引用:使用 WeakReference 类实现,其引用的对象在下次回收时必定会被回收,无论内存是否充足;
- 虚引用:使用 PhantomReference 类实现,设置虚引用不影响对象的生命周期,对象在任何时候都可能被回收,其存在的目的是为了在对象被回收时提供一种通知机制。
方法区的废弃常量和无用的类
在 Java 虚拟机规范中不要求虚拟机在方法区实现垃圾收集,且性价比很低,效率也低。
-
判断废弃常量:当常量池中的某个字面量不再被任何对象引用,即没有存活的引用指向它时,会在内存回收过程中被清理出常量池。
-
判断无用的类的 3 个条件:
- 该类所有实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
- 加载该类的 ClassLoader 已经被回收;
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
graph LR
垃圾收集算法 --> 标记-清除算法
垃圾收集算法 --> 复制算法
垃圾收集算法 --> 标记-整理算法
标记-清除算法
标记所有需要回收的对象,再统一回收所有被标记的对象。是最基础的收集算法。不足之处:
- 标记和清除两个过程效率都不高;
- 因为垃圾对象是不连续的,清除后会造成空间碎片,在后续需要分配大对象时,如无法找到足够大的连续空间将再次触发垃圾收集,频繁触发导致效率更低。
复制算法
为了解决效率问题,将内存分为两块区域,每次只使用其中一块,当一块用完时将存活对象移到另一块,将剩下的垃圾对象清理掉。不足之处是每次只能用一块区域,导致空间利用率降低。
标记-整理算法
标记所有需要回收的对象,将存活对象移到一端,然后直接清理掉端边界以外的内存。解决了空间碎片和空间浪费的问题,但每次触发时都需要暂停所有用户线程,很影响用户体验,特别是对老年代进行垃圾回收时,耗时几乎是年轻代的10倍多。
垃圾收集类型
- Minor/Young GC:发生在年轻代的垃圾收集,频率高,速度快;
- Major/Old GC:发生在老年代的垃圾收集,频率低,速度一般会比年轻代 GC 慢 10 倍以上;
- Full GC:发生在整个堆和方法区的垃圾收集,速度更慢,会导致应用出现长时间的 STW 停顿,频繁触发堆系统性能影响较大。
内存分配
对象的内存分配首先考虑的是新生代的 Eden 区。启用本地线程分配缓冲(TLAB)时,对象会优先在对应线程的 TLAB 区域内分配。当Eden区无法容纳新创建的对象,特别是当对象大小超过了剩余空间,或是对象大小超过 -XX:PretenureSizeThreshold 指定的阈值时,虚拟机会触发一次 Minor GC。
Minor GC 过程中,Eden 区和其中一个 Survivor 区(例如 S0)中存活的对象会被复制到另一个 Survivor 区(例如 S1)。每次 Young GC 后,对象年龄加 1。经过多次 Minor GC(默认是 15 次),如果对象仍存活且年龄达到阈值,则会被晋升至老年代。而在 Survivor 区之间的对象复制过程中,始终遵循 from 区的对象复制到当前空闲的 to 区的原则。
垃圾收集器
-
Serial
- 单线程,使用复制算法,响应速度优先,停顿时间短
- 适用于单 cpu 环境下的 client 模式
-
ParNew
- 多线程版Serial,使用复制算法
- 使用多核并行回收,提高吞吐量,多线程并发时可能会增加系统暂停时间
- 一般与CMS配合使用与服务器端环境,适用于需要高吞吐量但能接受一定程度停顿的应用
-
Serial Old
- 串行,使用标记压缩算法,响应速度优先
- 单线程回收,处理大内存时效率低
- 单 CPU 环境下的 Client 模式,CMS 的后备预案。
-
Parallel Scavenge
- 多线程并行,使用复制算法,吞吐量优先
- 可自定义 GC 策略,通过参数调节达到优化吞吐量的目的,会带来较长的停顿时间
- 适用于在后台运算而不需要太多交互的任务。
-
Parallel Old
- 多线程并行,使用标记压缩算法,吞吐量优先
- 提供比 Serial Old 更高的并行回收能力,提升老年代的吞吐量,停顿时间较长
- 与 PS 搭配,适用于在后台运算而不需要太多交互的任务。
-
CMS
- 并发,使用标记清除算法,响应速度优先
- 并发标记阶段不会停止用户程序,减少 STW
- 但内存碎片化严重,容易导致频繁的 Full GC。
- 适用于对响应时间敏感的应用,如互联网服务
-
G1
- 并发,使用并发标记+压缩算法,将整个堆划分为多个 Region,并具有预测性停顿时间模型
- 全局并发标记和局部并发整理,实现更细粒度的内存管理,目标是尽量平衡吞吐量和停顿时间
- 但复杂度相对较高,对机器配置有一定要求,内存越大优势越明显
- 适用于大型服务器应用,堆内存超过6GB以上,需要低停顿时间切内存资源充足的情况