JVM

JVM的内存模型

根据 JDK 8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

1743757365202.png

程序计数器

::可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的 Java 方法的 JVM 指令地址。如果线程执行的是 Native 方法,计数器值为 null。

Java 虚拟机栈

每个线程都有自己独立的 Java 虚拟机栈,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。可能会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 虚拟机栈 局部变量表

栈中存的到底是指针还是对象?

在JVM内存模型中,栈(Stack)主要用于管理线程的局部变量和方法调用的上下文,而堆(Heap)则是用于存储所有类的实例和数组。
当我们在栈中讨论”存储”时,实际上指的是存储基本类型的数据(如int,double等)和对象的引用,而不是对象本身。

本地方法栈

与 Java 虚拟机栈类似,主要为虚拟机使用到的 Native 方法服务,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样可能出现 StackOverflowError 和 OutOfMemoryError 两种错误。

Java 堆

是 JVM 中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

1743763685174.png

JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

堆的几个部分

  • 新生代(Young Generation):新生代分为Eden Space和Survivor Space。在Eden Space中,大多数新 创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为SO(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。

  • 老年代(Old Generation/Tenured Generation):存放过一次或多次Minor GC仍存活的对象会被移动到 老年代。老年代中的对象生命周期较长,因此Major GC(也称为FullGC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。

  • 元空间(Metaspace):从Java 8开始,永久代(Permanent Generation)被元空间取代,用于存储类的 元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。

  • 大对象区(Large Object Space /Humongous Objects):在某些JVM实现中(如G1垃圾收集器),为 大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。

方法区(元空间)

在 JDK 1.8 及以后的版本中,方法区被元空间取代,使用本地内存(1.7在JVM内存中,1.8在系统内存中)。用于存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然方法区被描述为堆的逻辑部分,但有 “非堆” 的别名。

  • 类信息:包括类的结构信息、类的访问修饰符、父类与接口等信息。

  • 常量池:存储类和接口中的常量,包括字面值常量、符号引用,以及运行时常量池。

  • 静态变量:存储类的静态变量,这些变量在类初始化的时候被赋值。(JDK 7 及之后,HotSpot 已经把原本放在永久代的字符串常量池、静态变量等移动到堆中)

  • 方法字节码:存储类的方法字节码,即编译后的代码。

  • 符号引用:存储类和方法的符号引用,是一种直接引用不同于直接引用的引用类型。

  • 运行时常量池:存储着在类文件中的常量池数据,在类加载后在方法区生成该运行时常量池。

  • 常量池缓存:用于提升类加载的效率,将常用的常量缓存起来方便使用。

    1743828055179.png

运行时常量池

是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,具有动态性,运行时也可将新的常量放入池中。

直接内存

不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。直接内存的使用受到本机总内存的限制,若分配不当,可能导致 OutOfMemoryError 异常。

内存泄漏和内存溢出的理解?

内存泄露:内存泄漏是指程序在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致可用内存逐渐减少。虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。
内存泄露常见原因:

  • 静态集合:使用静态数据结构(如HashMap 或ArrayList)存储对象,且未清理。
  • 事件监听:未取消对事件源的监听,导致对象持续被引用。
  • 线程:未停止的线程可能持有对象引用,无法被回收。

内存溢出:内存溢出是指Java虚拟机(JVM)在申请内存时,无法找到足够的内存,最终引发OutOfMemoryError。这通常发生在堆内存不足以存放新创建的对象时。
内存溢出常见原因:

  • 大量对象创建:程序中不断创建大量对象,超出JVM堆的限制。
  • 持久引用:大型数据结构(如缓存、集合等)长时间持有对象引用,导致内存累积。
  • 递归调用:深度递归导致栈溢出。

JMM

对于 Java 来说,你可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。


指令重排序

简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。

常见的指令重排序有下面 2 种情况:

  • 编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
  • 指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

Java 内存模型的抽象示意图

JMM(Java 内存模型)

关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作(了解即可,无需死记硬背):

  • 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
  • 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
  • use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

happens-before 原则

JSR 133 引入了 happens-before 这个概念来描述两个操作之间的内存可见性。

为什么需要 happens-before 原则? happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化。happens-before 原则的设计思想其实非常简单:

  • 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行。
  • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序。

我们再来看看 JSR-133 对 happens-before 原则的定义:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序。

类初始化和类加载

.class文件

1743767191204.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; //Class 文件的标志 0xCAFEBABE 4 个字节
u2 minor_version;//Class 的小版本号 2B
u2 major_version;//Class 的大版本号 2B
u2 constant_pool_count;//常量池中的数量,表示接下来有多少cp_info
cp_info constant_pool[constant_pool_count-1];//常量池项
u2 access_flags;//Class 的访问标记和属性修饰符:
u2 this_class;//当前类全限定名称
u2 super_class;//父类全限定名称
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//字段数量
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}

img

field info(字段表) 的结构:

字段表的结构

在Java类文件中,字段的元数据通过字段表(field_info)结构描述,具体字段含义如下:


1. access_flags(访问标志)

  • 作用:通过位掩码(2字节)表示字段的访问权限和属性。
  • 常见标志位
    • 作用域修饰符
      • ACC_PUBLIC (0x0001):public,全局可见。
      • ACC_PRIVATE (0x0002):private,仅本类可见。
      • ACC_PROTECTED (0x0004):protected,本类及子类可见。
    • 类/实例变量
      • ACC_STATIC (0x0008):static,表示类变量(静态变量)。
    • 可变性
      • ACC_FINAL (0x0010):final,字段不可修改。
    • 可见性与同步
      • ACC_VOLATILE (0x0040):volatile,确保多线程下的可见性,禁止指令重排序。
    • 序列化控制
      • ACC_TRANSIENT (0x0080):transient,字段不被序列化。

2. name_index(名称索引)

  • 作用:指向常量池中的CONSTANT_Utf8_info条目,存储字段的名称
  • 示例:若字段名为count,则name_index指向常量池中值为"count"Utf8条目。

3. descriptor_index(描述符索引)

  • 作用:指向常量池中的CONSTANT_Utf8_info条目,描述字段的类型
  • 描述符格式
    • 基本类型
      • IintJlongFfloatDdoubleCcharZbooleanBbyteSshort
    • 引用类型
      • Ljava/lang/String;String类,[Iint[]数组。

4. attributes_count(属性数量)

  • 作用:表示字段的附加属性数量。
  • 说明:字段可携带额外信息(如默认值、注解等),attributes_count记录这些属性的个数。

5. attributes[attributes_count](属性表)

  • 作用:存储字段的附加属性,每个属性为attribute_info结构。
  • 常见属性类型
    • ConstantValue
      • 用于static final字段,指定常量初始值(如public static final int MAX = 100;)。
    • Synthetic
      • 标记字段由编译器生成(如内部类访问外部类字段的桥接字段)。
    • Deprecated
      • 标记字段已过时(通过@Deprecated注解生成)。
    • Signature
      • 存储泛型字段的类型签名(如List<String>对应Ljava/util/List<Ljava/lang/String;>;)。

类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

一个类的完整生命周期

类加载过程

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

1743830959879.png

加载

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流。
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

验证

**验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。**验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查)
  2. 元数据验证(字节码语义检查)
  3. 字节码验证(程序语义检查)
  4. 符号引用验证(类的正确性检查)

准备

准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

初始化

初始化阶段是执行初始化方法 <clinit> ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

  • 生成条件:当类中存在静态变量赋值语句静态代码块时,编译器会自动生成<clinit>方法,用于合并这些静态初始化逻辑。
  • 执行内容:按代码顺序执行静态变量的显式赋值及静态代码块中的操作。

对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):

  1. 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时:

    • new: 创建一个类的实例对象。
    • getstaticputstatic: 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)。
    • invokestatic: 调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forName("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。

  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。

  5. .MethodHandleVarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 findStaticVarHandle 来初始化要调用的类。

  6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

类卸载

卸载类即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

所以,在 JVM 生命周期内,由 jvm 自带的类加载器加载的类是不会被卸载的。但是由我们自定义的类加载器加载的类是可能被卸载的。

创建对象的过程?

1743828669292.png

在Java中创建对象的过程包括以下几个步骤:

1.类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

2.分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存

3.初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)

4.进行必要设置,比如对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5.执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始——构造函数,即class文件中的方法还没有执行,所有的字段都还为零,对象需要的其他资源和状态信息还没有按照预定的意图构造好。所以一般来说,执行new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来。(实例变量)

类加载器

1743830265709.png

getParent()获取其父 ClassLoader为什么 获取到 ClassLoader 为null就是 bootstrapClassLoader 加载的呢?** 这是因为BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

双亲委派模型的好处

双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载防止核心 API 被篡改

JVM 区分不同类的依据是类名加上加载该类的类加载器,即使类名相同,如果由不同的类加载器加载,也会被视为不同的类。 双亲委派模型确保核心类总是由 BootstrapClassLoader 加载,保证了核心类的唯一性。

垃圾回收

什么是Java里的垃圾回收?如何触发垃圾回收?

垃圾回收(Garbage Collection,GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:

  • 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求:虽然垃圾回收是自动的,开发者可以通过调用system.gc()或
    Runtime.getRuntime().gc()建议JVM进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数:启动Java 应用时可以通过JVM参数来调整垃圾回收的行为,比如:
    -Xmx(最大堆大 小)、 -xms(初始堆大小)等。
  • 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

判断垃圾的方法有哪些?

垃圾回收算法来实现:引用计数法可达性分析算法

引用计数法:对象分配一个引用计数器

缺点::不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。

可达性分析:原理:从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Rootsi没有任何引用链相连(即从GC Roots:到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI(Java NativeInterface)引用的对象、活跃线程的引用等。

垃圾回收算法有哪些?

  • 标记—清除算法:标记—清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象(亦或是存活对象),然后统一回收所有被标记的对象。

    标记—清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次GC。

  • 复制算法:复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足

  • 标记—整理算法:复制算法在GC之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在GC之后的存活率就比较高,所以就有人提出了“标记—整理算法”。标记—整理算法的“标记“过程与“标记—清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。

  • 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的GC次数。对象创建时,一般在新生代申请内存,当经历一次GC之后如果对还存活,那么对象的年龄+1。当年龄超过一定值(默认是15,可以通过参数—XX:MaxTenuringThreshold来设定)后,如果对象还存活,那么该对象会进入老年代。

垃圾回收器

1743833828838.png

提高吞吐量(用户代码运行时间/总时间),扩大堆内存,回收次数少了,但一次回收时间长了

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old

CMS

1743834057868.png

G1

1743834318653.png

minorGC、majorGC、fullGC的区别,什么场景触发full GC

  • minor GC:新生代的垃圾回收,频率高,停顿时间短。

触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(OldGeneration)。

  • major GC:老年代的垃圾回收,频率低停顿时间长(存活的对象多,标记和整理的过程较为复杂)。

·触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。

  • full GC:整个堆内存的垃圾回收,频率最低,停顿时间最长。

触发条件:

1.直接调用System.gc()或Runtime·getRuntime()·gc()方法时,虽然不能保证立即执行,但JVM会尝试执行FullGC。

2.MinorGC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发FulGC,对整个堆内存进行回收。

3.当永久代(Java8之前的版本)或元空间(Java8及以后的版本)空间不足时。

在Java应用中,选择CMS(Concurrent Mark Sweep)或G1(Garbage-First)垃圾收集器需综合考虑内存大小、应用场景、性能需求及JDK版本等因素。以下是两者的适用场景及对比分析:


什么情况下使用CMS,什么情况使用G1?

一、CMS的适用场景

1. 内存较小的应用(堆内存通常 < 6GB)

  • 原因:CMS在小内存场景下表现更优,因其并发标记和清除的设计能减少停顿时间,且内存碎片问题在较小堆中影响较小。示例:客户端应用、轻量级Web服务。

2. 对响应时间敏感但能容忍短暂Full GC

  • 原因:CMS的目标是最短停顿时间,适用于需要快速响应的场景(如实时系统、B/S架构服务),但需注意其可能因内存碎片触发Full GC。典型场景:高并发交互式应用(如电商、在线游戏)。

3. 使用较旧JDK版本(如JDK 8及以下)

  • 原因:CMS在JDK 9后被标记为弃用,JDK 14中移除,旧版本中CMS是默认或推荐选项。

4. CPU资源充足且核心数较多

  • 原因:CMS的并发标记阶段会占用较多CPU资源(默认线程数为 (CPU数量 + 3)/4),多核环境下可缓解吞吐量下降问题。

二、G1的适用场景

1. 大内存应用(堆内存通常 ≥ 6GB)

  • 原因:G1通过分区(Region)管理堆内存,支持TB级堆且停顿时间可控,避免CMS的内存碎片问题。示例:大数据处理、企业级后台服务。

2. 需要可预测的停顿时间

  • 原因:G1允许通过 -XX:MaxGCPauseMillis 设置最大停顿目标(默认200ms),适合对延迟敏感的应用(如金融交易系统)。

3. 避免Full GC或内存碎片

  • 原因:G1采用标记-整理算法,减少内存碎片,且通过混合回收(Mixed GC)逐步清理老年代,降低Full GC风险。

4. 使用较新JDK版本(如JDK 9+)

  • 原因:JDK 9后G1成为默认收集器,且在新版本中持续优化(如巨型对象回收、ZGC补充),适合长期维护的项目。

5. 多核服务器环境

  • 原因:G1的并行回收和分区策略能充分利用多核CPU资源,提升吞吐量。

三、CMS vs G1核心对比

维度 CMS G1
内存管理 标记-清除,易产生碎片 标记-整理,无碎片
停顿时间 低停顿,但Full GC不可控 可预测停顿(通过参数设置)
适用堆大小 < 6GB ≥ 6GB
CPU资源占用 高(并发阶段占用多线程) 中等(分区回收优化资源分配)
JDK版本支持 JDK 8及以下 JDK 7+(推荐JDK 9+)
设计目标 低延迟 平衡吞吐量与低延迟

总结

CMS适用于小内存、低延迟但能容忍调优复杂性的场景,而G1更适合大内存、需稳定响应及长期维护的应用。随着JDK版本升级,G1逐渐成为主流,尤其在JDK 11后结合ZGC等新技术,进一步优化了超大堆和低延迟需求。