垃圾收集器
垃圾收集(Garbage Collection,简称GC),垃圾回收需要考虑三件事:哪些内存需要回收、什么时候回收、如何回收?
在Java中主要在于堆内存与方法区的回收。
JVM内存模型
这里放一张JVM的体系结构图,画的还是比较清晰。出自JVM内存模型和结构详解
哪些需要回收?
在进行垃圾回收第一件事需要确定的是哪些对象已死(即不再被任何途径使用的对象)。
引用计数算法(Reference Counting)
在对象当中添加一个计数器,当对象被引用时计数器值加一,引用失效时,计数器值减一。该算法非常简单,但比较直观。但单纯的计数算法存在一些缺陷,当两个对象相互依赖时会导致永远无法GC。
可达性分析算法(Reachability Analysis)
算法的基本思路就是,从根节点(GC Roots)出发,根据引用关系向下搜索,如果某个对象到根节点无任何引用链或者不可达,则证明该对象不再被使用即可以被回收。
在Java中,固定可作为GC Roots的对象包括:
-
在虚拟机栈(栈帧中的本地变量表)中引用的对象包括方法中定义的局部变量、方法参数、临时变量等。
public void example() { Object obj = new Object(); // obj 是 GC Roots }
-
在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
public class MyClass { static Object obj = new Object(); // obj 是 GC Roots }
-
方法区中常量引用的对象,被 final static 修饰的常量对象,如字符串常量池。
-
本地方法栈中 JNI(即 Native 方法)引用的对象。
-
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
引用
强引用
强引用是最传统的"引用"的定义,是指在程序代码之中普遍存在的引用赋值,即类似"Object obj=new Object()"这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用
虚引用也称为"幽灵引用"或者"幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
垃圾收集算法
分代收集理论
1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
标记-清除算法
该算法由Lisp之父JohnMcCarthy提出,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收所有被标记的对象。标记算法有两个主要缺点:第一个是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法是最基础的收集算法,大多数算法都是基于该算法进行改进。
标记-复制算法
标记-复制算法常被简称为复制算法。为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969年Fenichel提出了一种称为"半区复制"(Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
标记-整理算法
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
经典垃圾收集器
"如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。" (周志明, 2019)
Serial收集器
Serial是单线程工作的收集器,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束(Stop The World)。
ParNew收集器
ParNew 收集器是 Serial 收集器的多线程版本,属于年轻代垃圾回收器,采用复制算法进行回收,具有并行处理能力,常与 CMS 收集器搭配使用,适用于低延迟、多核环境。其特点包括:支持多线程回收(可通过参数设置线程数)、使用复制算法避免内存碎片、在执行时会暂停用户线程(Stop-the-World)、是唯一能与 CMS 搭配的年轻代收集器。
Parallel Scavenge收集器
Parallel Scavenge 收集器 是一个使用复制算法的多线程年轻代垃圾回收器,强调的是高吞吐量 和可控制的停顿时间 ,常用于对系统整体性能要求较高的后台服务。它的主要特点包括:采用多线程并行回收、使用复制算法避免内存碎片、可以通过参数精细控制停顿时间与吞吐量(如 -XX:MaxGCPauseMillis
、-XX:GCTimeRatio
)、与老年代的 Parallel Old 收集器配合使用,形成"吞吐量优先型组合",适合 CPU 资源充足、对响应时间要求不高的场景。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。
Parallel Old收集器
Parallel Old 收集器 是老年代的多线程垃圾回收器,采用标记-整理(Mark-Compact)算法 ,主要强调高吞吐量和高处理效率 ,通常与年轻代的 Parallel Scavenge 收集器 配合使用,适用于对系统整体吞吐量要求较高的后台服务。其特点包括:支持多线程并行回收、采用标记-整理算法减少内存碎片、Stop-the-World 机制、适用于多核 CPU 和长时间运行的高性能应用场景。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
GC过程
阶段 | 是否并发 | 说明 |
---|---|---|
初始标记(Initial Mark) | ❌ STW | 标记 GC Roots 直接关联的对象,时间很短 |
并发标记(Concurrent Mark) | ✅ | 从 GC Roots 开始遍历对象图,标记存活对象 |
重新标记(Remark) | ❌ STW | 处理并发标记阶段遗漏的引用变动,耗时略长 |
并发清除(Concurrent Sweep) | ✅ | 清除未标记的对象,回收内存 |
并发重置(Concurrent Reset) | ✅ | 重置 CMS 内部数据结构,为下一次 GC 做准备 |
可选:预清理(Concurrent Preclean) | ✅ | 为降低重新标记时间,提前处理部分引用变动(非必须阶段) |
优点
低延迟:并发执行,最小化 STW 停顿时间,适合对响应时间敏感的系统(如 Web 应用、交易系统)。
高并发利用多核 CPU :并发标记/清除阶段可以与用户线程同时进行。垃圾回收响应更平滑:不会像其他 GC 那样突然产生长时间停顿。
缺点
容易产生内存碎片:使用标记-清除算法,清除后不整理内存(可选压缩需 Full GC)。
并发失败(Concurrent Mode Failure) :如果在并发回收还未完成时,老年代空间就被新对象占满,会触发一次 STW 的 Full GC,影响性能。
CPU 占用高:并发阶段会占用部分 CPU 资源,可能影响程序运行,尤其在 CPU 数量少的系统中表现明显。
G1收集器
G1(Garbage First)收集器 是 Java HotSpot 虚拟机为了解决 CMS 的缺点(如碎片、并发失败)而推出的 面向服务端应用的低停顿收集器 ,自 JDK 9 起成为默认 GC 。
内存结构
G1 把堆划分为多个大小相等的 Region(区域),每个 Region 可以扮演不同角色:
Region 类型 | 说明 |
---|---|
Eden 区 | 新生对象分配区 |
Survivor 区 | 新生代幸存对象 |
Old 区 | 老年代对象 |
Humongous 区 | 存放特别大的对象(超过一个 Region 大小的一半) |
Free 区 | 空闲区域,等待分配 |