内存管理与垃圾回收

内存区域划分

运行时数据区(Runtime Data Area)是指Java虚拟机在执行Java程序时,为了保证程序的正确运行而动态分配和使用的内存区域。这部分内存区域随着线程的创建和销毁而动态改变,是JVM在执行Java代码时实际使用的内存部分。

这些区域构成了JVM运行时内存模型的基础,其中,程序计数器、虚拟机栈和本地方法栈都是线程私有的,而Java堆和方法区则是所有线程共享的。运行时数据区的管理对于确保Java程序的正确执行和性能优化至关重要。例如,垃圾收集机制主要针对Java堆进行,而方法区的管理则涉及到类的加载和卸载过程。

下面是一个经典的JVM内存区域划分图(JDK1.8以前):

JDK 1.8 之前的版本(如 JDK 1.6 和 JDK 1.7)和 JDK 1.8 在内存区域划分上有显著的不同,尤其是涉及到方法区的实现。JDK8前后的运行时内存变化(Hotspot):

方法区(在JDK 1.8中更改为元数据区/Metaspace)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。在JDK 1.7及以前的版本中,方法区被称为永久代(PermGen space),并位于堆中。从JDK 1.8开始,方法区被移动到本机内存中,称为Metaspace,它不在Java堆中,也不受垃圾回收的影响,而是受到系统本机内存限制。

程序计数器

程序计数器( Program Counter Register ) : 负责跟踪当前线程所执行的字节码指令的地址。尽管程序计数器在JVM的整体内存模型中占据的空间相对较小,但它在确保线程切换和恢复执行的准确性方面发挥着至关重要的作用。其主要功能与作用如下:

  1. 指令跟踪: ==程序计数器存储了下一条要执行的字节码指令的地址==。在多线程环境中,每个线程都有自己的程序计数器,确保了线程之间执行的独立性和顺序性。

  2. 线程切换: 当线程被暂停或等待CPU时间片时,程序计数器的值会被保存,以便在线程恢复执行时能准确地从上次停止的地方继续运行。

  3. 分支和循环控制与异常处理: 对于条件语句(如if-else)、循环语句(如for、while)和跳转指令,程序计数器会根据控制流逻辑更新其值,指向适当的指令地址。 在遇到异常时,程序计数器可以帮助定位异常发生的指令位置,这对于异常处理和调试非常重要。

程序计数器通过跟踪字节码指令的执行位置,确保了多线程环境下程序的正确执行流程。它的线程私有属性和对指令执行的精确控制,使得JVM能够高效地管理多个并发线程,同时保证每个线程的执行独立性和安全性。

虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack,简称JVM栈)负责管理线程的局部变量和方法调用过程中的临时数据。每个线程在其生命周期中都会拥有一个独立的JVM栈。

==当一个方法被调用时,JVM栈会创建一个新的栈帧(Stack Frame),用于存储该方法的局部变量、操作数栈、动态链接信息和返回地址==等。当方法执行完毕,其对应的栈帧会被弹出,释放占用的资源。 每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈帧是JVM栈的基本单元,通常包含以下部分:

  • 局部变量表(Local Variable Table):存储方法参数和局部变量。
  • 操作数栈(Operand Stack):用于暂存中间计算结果,以及执行算术运算、方法调用等操作。
  • 动态链接信息:支持方法调用过程中对常量池中符号引用的解析,即将符号引用转换为直接引用的过程。
  • 返回地址:指出方法返回后应继续执行的下一条指令的位置。
  • 附加信息:可能还包括一些辅助信息,如线程锁、异常处理器等。

例:在JVM中 执行 c = a + b 的字节码执行过程中操作数栈以及局部变量表的变化如下图所示

java
public static void main(String[] args) {
    int a = 11;
    int b = 12;
    int c = a + b;
}

第一步:将变量存储到局部变量表 bipush 用于将一个小整数推入操作数栈,istore 用于从操作数栈弹出一个整数并存储到局部变量表中。

第二步:加载a和b的值iload 用于将局部变量表中的整数加载到操作数栈)

  1. 当JVM执行到 iload_1 时,它会从局部变量表中加载变量 a 的值(11)到操作数栈。
  2. 接着执行第 iload_2,从局部变量表中加载变量 b 的值(12)到操作数栈。 此时,操作数栈顶部有两个值:12 和 11(注意栈的后进先出特性)。

第三步:执行加法操作iadd 用于执行两个整数的加法操作)

  1. 执行 iadd 指令时,JVM会从操作数栈中弹出顶部的两个值(12 和 11),执行加法操作,得到结果 23。
  2. 加法操作的结果(23)被重新压入操作数栈。

第四步:存储结果 执行 istore_3 指令,从操作数栈中弹出结果 23,并将其存储到局部变量表中索引为3的位置,即变量 c

本地方法栈

本地方法栈(Native Method Stack)的主要作用是==支持本地方法(Native Methods)的执行==。当Java代码中调用了本地方法时,JVM会利用本地方法栈来管理和协调本地方法的执行。这包括方法调用的上下文切换、参数传递、返回值接收等。

本地方法栈与虚拟机栈的区别:

  • 服务对象不同:虚拟机栈服务于Java方法,负责执行字节码;而本地方法栈服务于本地方法,这些方法不是用Java编写的,而是用其他语言(如C/C++)编写并被编译成机器码的。

  • 实现方式差异:由于本地方法栈的服务对象不同,它的实现方式也没有严格的规定。有些JVM实现可能将本地方法栈和虚拟机栈合并在一起,如HotSpot JVM;而有些JVM则可能保持独立的本地方法栈。

本地方法通常用于以下几种情况:

  • 需要高性能的底层操作,如密集型数学计算或文件I/O。
  • 与操作系统或硬件紧密相关的功能,如图形界面或设备驱动。
  • 需要调用已有的C/C++库,以重用现有代码或集成第三方组件。

堆(Heap)

堆(Heap)主要用于==存储Java对象实例和数组==。 Java堆是被所 有线程共享 的一块内存区域, 在虚拟机启动时创建。

在JDK 1.8及之前的版本中,堆内存被划分为几个主要的区域(从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆可以细分为):

  1. 年轻代(Young Generation)
  2. 老年代(Old Generation)
  3. 永久代(Permanent Generation)

从JDK 1.8开始,永久代被元空间(Metaspace)取代,元空间使用的是本机内存而不是堆内存。因此,堆内存划分变为:年轻代(Young Generation)和老年代(Old Generation)

在Java8以后,方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中

配置新生代和老年代堆结构占比

  • 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms-Xmx 来指定

  • 新生代和老年代堆结构占比: 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3。 修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5

  • Eden空间和另外两个Survivor空间占比分别为8:1:1 ,可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8

几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁

堆(Heap)的特点总结:

  • 堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动的时候创建。几乎所有的对象实例以及数组都要在这里分配内存。
  • 堆是jvm所有线程共享的。 堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
  • 堆是垃圾收集器管理的主要区域,因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。
  • 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
  • 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
  • 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常,即:堆内存溢出:java.lang.OutofMemoryError : java heap space.

产生 OutOfMemoryError 异常的常见原因:

  • 内存中加载的数据过多,如一次从数据库中取出过多数据;
  • 集合对对象引用过多且使用完后没有清空;
  • 代码中存在死循环或循环产生过多重复对象;
  • 堆内存分配不合理

方法区

方法区(Method Area)是运行时数据区的一部分,用于存储每个类的信息(Class Metadata)、常量、静态变量、即时编译器编译后的代码等数据。方法区有时(对HotSpot而言)也被称为非堆(Non-Heap)或元数据区。

方法区只是一个规范,其实现方式在jdk1.7及之前为永久代,jdk1.8则为元空间(MetaSpace),且元空间存在于本地内存(Native Memory)

Java8为什么要将永久代替换成Metaspace ?

  • 字符串存在永久代中,容易出现性能问题和内存溢出
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

查看永久代和元空间相关参数的命令:

shell
jps  # 是java提供的一个显示当前所有java进程pid的命令

jinfo -flag PermSize 进程号     #查看进程的PermSize初始化空间大小
jinfo -flag MaxPermSize 进程号  #查看PermSize最大空间

jinfo -flag MetaspaceSize 进程号     #查看Metaspace 最大分配内存空间
jinfo -flag MaxMetaspaceSize 进程号  #查看Metaspace最大空间

::: details 关于永久代和元空间的历史简介

在JDK 8以前,许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈。

本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。

但是对于其他虚拟机实现,譬如BEA JRockit、IBM J9等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。

但现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法(例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。

当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java MissionControl管理工具,移植到HotSpot虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的 元空间(Meta-space) 来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。 :::

关于intern()方法在不同JDK版本中的区别:

  • 在JDK 1.6及之前版本中,如果串池中已经存在则直返回,不存在则==复制到串池中并返回其引用==
  • JDK 1.7后的intern方法实现不会再复制一份字符串实例到串池,而是==直接在串池中保存了对这个堆上的字符串实例的引用==,这样可以减少内存的占用。
java
String ab = new String("a")  new String("b");  //在堆中创建"a","b","ab";

//先判断StringTable中是否有"ab",如果有直接返回StringTable中的地址
String ab2 = ab.intern();  // 如果没有则将该对象放入StringTable中(注意不同版本的区别点)

String ab3 = "ab";  //因为StringTable中有"ab",此时直接返回StringTable中地址。

System.out.println( ab2 == ab3 ); // true        

System.out.println( ab == ab3 );  // JDK1.6为 false 在JDK 1.7以后版本结果是true

直接内存

直接内存(Direct Memory)是一个特殊的内存区域,它不属于JVM的标准堆内存或方法区,而是直接向操作系统申请的堆外内存。直接内存的引入主要是为了支持高性能的I/O操作,尤其是在Java NIO(New IO)框架中,通过使用直接内存可以显著提高数据传输的效率。

  1. 堆外内存: 直接内存是在JVM堆之外分配的,它不受垃圾回收(GC)的影响,因此不会在常规的垃圾收集过程中被回收,除非通过特定的机制显式回收。

  2. 性能优势: 使用直接内存可以避免在Java堆和本机堆之间复制数据,因为数据可以直接在本机堆和设备(如磁盘或网络接口)之间传输,这在大数据量的I/O操作中尤其重要,可以减少CPU的复制操作,提高性能。

  3. 分配与回收: 直接内存的分配和回收成本较高,分配时需要通过JNI(Java Native Interface)或sun.misc.Unsafe类来完成。回收通常需要手动进行,或者当DirectByteBuffer对象不再被引用时,JVM会触发一个清理操作,通知操作系统释放内存。

在Java中,直接内存可以通过java.nio.ByteBufferallocateDirect()方法来分配和使用。以下是一个简单的示例:

java
import java.nio.ByteBuffer;

public class DirectMemoryExample {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        // 使用buffer...
    }
}

在这个例子中,ByteBuffer.allocateDirect(1024)会直接向操作系统申请1024字节的内存,并返回一个DirectByteBuffer对象,该对象充当了直接内存的引用。

直接内存是JVM提供的一种堆外内存管理机制,主要用于优化I/O操作的性能。它不受常规的垃圾回收机制影响,因此需要程序员更加谨慎地管理其分配和回收,以避免潜在的内存泄漏和资源浪费问题。

垃圾回收基本概念

垃圾回收(Garbage Collection,简称GC)是现代编程语言和运行环境中的一个重要组成部分,尤其是在Java这样的自动内存管理语言中。

Java中堆是进行 GC 的主要区域,堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定哪些对象是需要进行垃圾回收的。

垃圾回收机制通过识别和回收不可达对象来管理内存,利用代际假说来优化回收策略,==通过可达性分析算法来确定哪些对象是活动的==。这些机制共同作用,使得Java程序无需手动管理内存,减少了内存泄漏和野指针等问题,提高了程序的健壮性和可维护性。

可达性分析

可达性分析(Reachability Analysis) 是垃圾回收算法用来确定对象是否可达的方法。 它通常从一组 ==根对象(GC Roots)== 开始,这组根对象集通常包括:

  • 所有活动线程的栈帧中的局部变量。
  • 方法区(即永久代或元空间)中的静态变量。
  • 方法区中的某些常量引用。
  • 本地方法栈中JNI(Java Native Interface)的引用。

可达性分析算法会从这些根对象集开始,沿着对象之间的引用链进行深度优先或广度优先搜索,标记所有能从根对象直接或间接引用到的对象。未被标记的对象被视为不可达,可以被垃圾回收器回收。

::: details 关于引用计数法 当前主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过 可达性分析(Reachability Analysis) 算法来判定对象是否存活的。 Java 虚拟机(JVM)不使用引用计数法来确定对象是否可回收,主要原因有以下几点:

1. 循环引用问题

引用计数法的一个主要缺陷是无法处理循环引用。假设有两个对象互相引用彼此,即使它们不再被外部代码引用,它们的引用计数也不会降为零,因此也不会被回收。下面是一个简单的例子:

java
class Node {
    Node next;
}

Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;

// Now, both 'a' and 'b' are referenced by each other, 
// but not reachable from any GC roots
a = null;
b = null;

System.gc();  

在这种情况下,ab 对象虽然断开了与GC Root的连接,但由于它们互相引用,引用计数永远不会降为零,从而导致内存泄露。

而可达性分析依赖于从GC Roots出发遍历对象引用链,通过标记所有可达对象,从而有效地处理了循环引用问题。

2. 性能问题

引用计数法需要在每次对象引用发生变化时都需要进行加减操作。这意味着,每当一个引用被创建或销毁时,即便是赋值操作也需要更新计数器,这会引入额外的性能开销。

3. 碎片化问题

引用计数不仅会增加时间上的性能开销,还会导致内存碎片化。因为引用计数法不能有效地整理和压缩内存,长期运行的系统容易产生大量碎片,降低内存的利用效率。

:::

不可达对象

不可达对象(Unreachable Objects) 是指那些不再被任何有效引用所指向的对象。这意味着如果一个对象不能从任何活动线程的根对象集(GC Roots)通过一系列引用链访问到,那么这个对象就被视为不可达,从而成为垃圾回收的候选对象。

一旦对象变为不可达,GC就可以在适当的时机回收它所占用的内存,以便重新分配给其他对象使用。

代际假说理论

代际假说(Generational Hypothesis) 是垃圾回收领域的一个重要理论,它基于以下观察:

  • 新创建的对象往往寿命较短,很快就会变得不可达,成为垃圾。
  • 如果一个对象在多次垃圾回收后仍然存活,那么它很可能会继续存活很长时间。

基于这个假说,JVM将堆内存划分为几个代(Generation):

  • 年轻代(Young Generation):新创建的对象首先在这里分配,通常这里发生频繁的垃圾回收。
  • 老年代(Old Generation):经过多次年轻代GC后存活下来的对象会被提升到老年代,这里发生垃圾回收的频率较低。

其他相关概念

Minor GC、Major GC、Full GC以及Mixed GC等概念,很大程度上源自HotSpot虚拟机的内部实现和相关文档。HotSpot是Oracle JDK和OpenJDK默认的JVM实现,它的垃圾回收策略和命名约定在Java社区中非常普及,以至于这些术语在讨论垃圾回收时常常被广泛使用。

这些概念是垃圾回收领域普遍存在的,并且在不同的JVM实现中,也有类似的功能和行为,尽管具体实现细节可能有所不同。

垃圾回收算法

垃圾回收算法是自动内存管理系统的关键部分,用于识别和回收不再使用的内存,防止内存泄漏,提高程序的稳定性和性能。

现代的垃圾回收器,如G1、ZGC和Shenandoah,往往采用复合算法,结合上述各种算法的优点,以适应复杂多变的运行环境。

标记-清除算法

标记-清除(Mark and Sweep) 是最基本的垃圾回收算法,分为两个阶段:

  • 标记阶段:从根对象(GC Roots)开始,遍历整个对象图,将所有可达对象标记为“活着”的状态。这通常通过深度优先搜索或广度优先搜索完成。
  • 清除阶段:回收未被标记的对象所占用的内存空间。所有未标记的对象被认为是垃圾,可以被回收。虽然内存空间被释放,但由于对象位置不变,可能会留下很多小块的空闲空间,造成内存碎片。

标记-复制算法

标记-复制算法常被简称为复制算法(Copying)。现在的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代

复制算法将可用内存分为两个相等的部分,每次只使用其中一个部分。当这部分的内存用完时,垃圾回收器会检查这部分内存中的对象,将存活的对象复制到另一部分的内存中,然后放弃原来的那部分内存。

标记-整理算法

标记-整理(Mark-Compact) 算法,有时也被称为标记-压缩算法,是在标记-清除算法的基础上增加了整理(Compact)步骤。

标记阶段:算法会像标记-清除算法那样,从根节点开始遍历整个对象图,标记所有可达的对象。

压缩阶段:与复制算法类似,但并非将存活对象复制到另一个区域,而是将它们移动到当前内存区域的一端,从而整理内存空间,消除碎片。这一过程中,所有存活对象的地址会被更新,以反映它们的新位置。

分代收集算法

分代收集(Generational Collection) 基于“代际假说”:新创建的对象倾向于较快地变成垃圾,而存活时间长的对象则倾向于继续存活。JVM将堆分为几代:

  • 年轻代(Young Generation):新对象首先分配在此,包含Eden和两个Survivor空间。
  • 老年代(Old Generation):多次存活的对象最终会被提升到这里。

年轻代使用复制算法,而老年代通常使用标记-清除标记-整理算法。

增量/并行/并发收集

增量收集(Incremental Collection)): 逐步进行垃圾回收以减少停顿时间,而不是一次性处理整个堆

并行收集(Parallel Collection): 多个CPU同时进行垃圾回收工作,利用多核处理器的优势加快回收速度

并发收集(Concurrent Collection): 允许部分GC过程与应用程序并行执行,进一步减少了停顿时间,例如CMS(Concurrent Mark Sweep)和G1(Garbage First)收集器。

每种垃圾回收算法都有其特定的适用场景和优化目标,现代的JVM通常会结合多种算法来提供更高效和灵活的内存管理。

常见垃圾回收器

Java虚拟机的HotSpot实现提供了多种垃圾收集器和算法,每种都有其特定的使用场景和优化目标。

可以使用-XX:+PrintCommandLineFlags选项来查看JVM启动时使用的默认参数,包括垃圾收集器的信息。例如:

bash
# 查看JVM启动时使用的默认参数,包括垃圾收集器的信息
java -XX:+PrintCommandLineFlags -version

# 查看JVM支持的所有垃圾收集器选项
java -XX:+PrintFlagsFinal -version | grep Use

Serial

Serial系列的垃圾回收器是Sun Microsystems(后来被Oracle收购)的HotSpot虚拟机团队开发的,==主要用于单线程的环境中,尤其是在客户端应用或者小型系统中==。它们的设计简单,易于理解和维护,但在多核处理器的现代系统上效率较低。

Serial系列主要包括两个主要的收集器:Serial收集器和Serial Old收集器

在JVM启动参数中,可以通过以下方式启用Serial系列的垃圾回收器:

  • -XX:+UseSerialGC:启用整个Serial GC,即同时使用Serial收集器和Serial Old收集器
  • -XX:+UseSerialCollector:仅启用年轻代的Serial收集器,老年代的收集器将由JVM自动选择
  • -XX:+UseSerialOldGC:仅启用老年代的Serial Old收集器,年轻代的收集器将由JVM自动选择

Serial系列的垃圾回收器虽然在性能上不如现代的并行或多线程垃圾回收器,但它们在某些特定场景下仍然有其价值,特别是对于那些不需要高端性能且希望保持简单性和稳定性的应用。

ParNew

ParNew系列的垃圾回收器同样是Sun Microsystems(现为Oracle)的HotSpot虚拟机团队的成果,它作为Serial收集器的多线程版本,专为年轻代(Young Generation)设计,以提升多核处理器环境下垃圾回收的效率。ParNew的引入是为了应对现代计算架构中并行处理能力的增长,从而提高整体系统的吞吐量和响应性。

在JVM启动参数中,ParNew收集器的启用方式如下:

  • -XX:+UseParNewGC:启用ParNew作为年轻代的垃圾收集器,通常会与指定的老年代收集器配合使用。

ParNew收集器通过并行化垃圾回收过程,显著提升了年轻代的垃圾回收效率,尤其在多核处理器系统中表现突出。尽管如此,随着JVM技术的发展,如G1和ZGC等更为先进的垃圾收集器逐步成熟,ParNew的应用场景逐渐受到限制,但它在并行垃圾回收领域的历史地位不可忽视。

Parallel

Parallel系列的垃圾回收器也是由HotSpot虚拟机团队开发,与Serial系列形成鲜明对比,旨在充分利用多核处理器的并行处理能力,提高垃圾回收效率,特别是在服务器端应用或高性能计算环境中。Parallel系列包括两个主要组件:Parallel Scavenge收集器和Parallel Old收集器,共同致力于优化吞吐量和系统资源利用率。

在JVM启动参数中,启用Parallel系列的垃圾回收器方式如下:

  • -XX:+UseParallelGC:启用Parallel GC,即同时使用Parallel Scavenge收集器和Parallel Old收集器。
  • -XX:+UseParallelOldGC:仅启用Parallel Old收集器,年轻代的收集器将由JVM自动选择,通常为Parallel Scavenge。
  • -XX:ParallelGCThreads=<N>:设置Parallel GC使用的线程数,<N>表示具体的线程数量。

Parallel系列的垃圾回收器,通过并行化垃圾回收过程,极大提升了系统的整体性能和资源利用率,特别适合于那些对吞吐量有高要求的大型服务器应用(如大型数据库服务、科学计算和大规模数据处理系统)。它们代表了HotSpot虚拟机团队在多核处理器时代对垃圾回收技术的创新和优化。

CMS

CMS (Concurrent Mark Sweep)收集器,由Sun Microsystems的HotSpot虚拟机团队设计开发,旨在最小化垃圾回收对应用程序的中断时间,特别适合于对响应时间敏感的应用场景,如Web服务器和交互式系统,其中低延迟比高吞吐量更为重要。

在JVM启动参数中,启用CMS收集器的方式如下:

  • -XX:+UseConcMarkSweepGC:启用CMS作为老年代的垃圾收集器。
  • -XX:CMSInitiatingOccupancyFraction=<N>:设置堆内存占用达到多少百分比时触发CMS的阈值,默认为92%。
  • -XX:ParallelCMSThreads=<N>:设置CMS并发标记和清除阶段的线程数。

CMS收集器以其低延迟特性在业界得到了广泛的应用,特别是在那些对用户体验高度敏感的环境中。然而,随着更先进收集器(如G1和ZGC)的出现,CMS的一些局限性(如内存碎片问题)得到了改善。

G1(Garbage-First)

G1收集器(Garbage-First Collector)是HotSpot虚拟机团队开发的一款面向服务端应用的垃圾回收器,设计目标是在可控的停顿时间内高效地回收垃圾,同时能够充分利用多核处理器的并行处理能力。

G1自JDK9起成为默认的垃圾收集器,在年轻代和老年代之间提供了一种统一的垃圾回收解决方案

在JVM启动参数中,启用G1收集器的方式如下:

  • -XX:+UseG1GC:启用G1作为垃圾收集器。
  • -XX:MaxGCPauseMillis=<N>:设置最大垃圾回收停顿时间的目标,<N>表示毫秒数。
  • -XX:G1HeapRegionSize=<N>:设置每个Region的大小,<N>表示字节大小。

G1收集器的引入标志着Java垃圾回收技术的一个重大进步,它不仅解决了过去垃圾收集器在多核处理器环境下效率不高的问题,还通过其独特的设计,提供了更佳的性能和更可控的延迟,特别适合于现代大型服务端应用的需求。随着JDK的不断演进,G1收集器已成为许多服务端应用的首选垃圾回收方案。

ZGC

ZGC(Z Garbage Collector)是Oracle为解决大规模堆内存管理问题而设计的一种低延迟、可扩展的垃圾回收器,特别适用于拥有大量内存的现代服务器环境

ZGC自JDK 11开始作为实验性特性加入,并在后续版本中逐渐成熟,其目标是在巨大的堆大小下也能保持极低的垃圾回收停顿时间。

在JVM启动参数中,启用ZGC收集器的方式如下:

  • -XX:+UnlockExperimentalVMOptions
  • -XX:+UseZGC

ZGC收集器代表了垃圾回收技术的前沿,它解决了大堆环境下垃圾回收的挑战,特别是对于那些对延迟极其敏感的应用场景,如金融交易系统、大数据处理平台等。尽管ZGC在JDK中作为实验性特性引入,但其性能和稳定性已经得到了社区的广泛认可,成为现代服务端应用中一个强有力的选择。

Shenandoah

Shenandoah是一款低延迟垃圾回收器,旨在提供与G1和ZGC类似的低停顿时间,同时在不同规模的堆上都能保持良好的性能。Shenandoah特别适合那些对响应时间有严格要求的应用程序,它能在大规模堆内存环境下保持微秒级的停顿时间

Shenandoah垃圾回收器最初由Red Hat公司开发,并在2014年贡献给了OpenJDK。它随后成为了Adoptium项目的一部分,与其他垃圾回收器如(G1和ZGC)一同为Java开发者提供了更多性能优化和延迟控制的选择。

在JVM启动参数中,启用Shenandoah收集器的方式如下:

  • -XX:+UnlockDiagnosticVMOptions
  • -XX:+UnlockExperimentalVMOptions
  • -XX:+UseShenandoahGC

Shenandoah收集器是为满足现代应用程序对低延迟和高性能的需求而设计的,它不仅适用于服务端应用,也适用于需要快速响应的客户端应用。Shenandoah在OpenJDK中的引入,为开发者提供了另一种在不同堆大小和硬件配置下都能保持良好性能的垃圾回收选择。