JVM

JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范。引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

JVM的内存结构

Java 虚拟机的内存空间分为 5 个部分:

  • 程序计数器
  • Java 虚拟机栈
  • 本地方法栈
  • 方法区
jvm-memory-structure

程序计数器

​ 可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的Java方法的JVM指令地址。如果线程执行的是Native方法,计数器值为null。是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,生命周期与线程相同。

程序计数器的特点

  • 是一块较小的内存空间。
  • 线程私有,每条线程都有自己的程序计数器。
  • 生命周期:随着线程的创建而创建,随着线程的结束而销毁。
  • 是唯一一个不会出现 OutOfMemoryError 的内存区域。

程序计数器的作用

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
  • 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。

Java 虚拟机栈(Java 栈)

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

Java 虚拟机栈是描述 Java 方法运行过程的内存模型。

Java 虚拟机栈会为每一个即将运行的 Java 方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法出口信息
  • ……
jvm-stack

本地方法栈(C 栈)

本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。它与 Java 虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。

​ 是JVM中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建,用于存放对象实例。从内存回收角度,堆被划分为新生代和老年代,新生代又分为Eden区和两个Survivor区(From Survivor和To Survivor)。如果在堆中没有内存完成实例分配,并且堆也无法扩展时会抛出Out Of Memory Error异常。

堆是用来存放对象的内存空间,几乎所有的对象都存储在堆中。

jvm-memory

方法区

​ 在JDK1.8及以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆的逻辑部分,但有“非堆”的别名。方法区可以选择不实现垃圾收集,内存不足时会抛出OutOfMemoryError异常。

  • 方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。

直接内存

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

JVM内存模型里的堆和栈有什么区别

1. 线程共享性

  • :所有线程共享的内存区域。堆中的对象实例可以被多个线程访问(需考虑线程安全)。
  • :每个线程私有,不共享。每个线程在创建时会分配独立的虚拟机栈,线程间的栈内存完全隔离。

2. 存储内容

  • 堆:主要存储对象实例和数组

    。例如,通过new Object()创建的对象、int[] arr = new int[10]定义的数组,其实际数据都存储在堆中。

  • 栈:存储栈帧

    (每个方法调用对应一个栈帧),栈帧中包含:

    • 局部变量表(存储基本数据类型:intchar等,以及对象的引用reference类型);
    • 操作数栈(方法执行时的临时数据操作);
    • 动态链接(指向运行时常量池的方法引用);
    • 方法出口(方法执行结束后返回的位置)。

3. 生命周期

  • :生命周期与 JVM 进程一致。只要 JVM 运行,堆就存在,直到 JVM 退出时才释放。
  • :生命周期与线程一致。线程创建时栈被初始化,线程结束时栈被销毁;栈中的栈帧则随方法调用(入栈)和结束(出栈)动态创建和销毁。

4. 大小与调整

  • 堆:是 JVM 中最大的内存区域,大小可通过参数调整(默认由 JVM 自动分配):
    • -Xms:初始堆大小;
    • -Xmx:最大堆大小(建议与-Xms设为相同,避免频繁扩容)。
  • :每个线程的栈大小较小(默认几百 KB),可通过-Xss参数调整(例如-Xss256k)。栈大小固定或有上限,无法动态扩容。

5. 垃圾回收

  • :是垃圾回收的核心区域。堆中对象实例若不再被引用(无可达性),会被垃圾收集器(如 G1、ZGC)回收,释放内存。
  • :无需垃圾回收。栈帧随方法结束自动出栈销毁,局部变量也随之释放,内存回收是 “确定性” 的。

6. 异常类型

  • :若内存不足无法分配新对象,会抛出OutOfMemoryError: Java heap space
  • 栈:
    • 若方法调用层级过深(如无限递归),栈帧数量超过栈容量,抛出StackOverflowError
    • 若创建线程时无法为栈申请到足够内存(如线程过多),抛出OutOfMemoryError: unable to create new native thread

堆分为哪几个部分

Java堆(Heap)是Java虚拟机(JVM)中内存管理的一个重要区域,主要用于存放对象实例和数组。随着JVM的发展和不同垃圾收集器的实现,堆的具体划分可能会有所不同,但通常可以分为以下几个部分:

img

新生代(YoungGeneration):新生代分为Eden Space和Survivor Space。在Eden Space中,大多数新创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次MinorGC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor1)。在每次MinorGC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两
个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。

老年代(OldGeneration/TenuredGeneration):存放过一次或多次MinorGC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此MajorGC(也称为FullGC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比MinorGC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。

大对象一般存储在哪个区域

大对象通常会直接分配到老年代。
新生代主要用于存放生命周期较短的对象,并且其内存空间相对较小。如果将大对象分配到新生代,可能会很快导致新生代空间不足,从而频繁触发MinorGC。而每次MinorGC都需要进行对象的复制和移动操作,这会带来一定的性能开销。将大对象直接分配到老年代,可以减少新生代的内存压力,降低MinorGC 的频率。
大对象通常需要连续的内存空间,如果在新生代中频繁分配和回收大对象,容易产生内存碎片,导致后续
分配大对象时可能因为内存不连续而失败。老年代的空间相对较大,更适合存储大对象,有助于减少内存
碎片的产生。

String s = new String(“abc”)执行过程中分别对应哪些内存区域?

​ 首先,我们看到这个代码中有一个new关键字,我们知道new指令是创建一个类的实例对象并完成加载初始化的,因此这个字符串对象是在运行期才能确定的,创建的字符串对象是在堆内存上。
​ 其次,在String的构造方法中传递了一个字符串abc,由于这里的abc是被final修饰的属性,所以它是一个
字符串常量。在首次构建这个对象时,JVM拿字面量“abc“去字符串常量池试图获取其对应String对象的引
用。于是在堆中创建了一个”abc”的String对象,并将其引用保存到字符串常量池中,然后返回;
​ 所以,如果abc这个字符串常量不存在,则创建两个对象,分别是abc这个字符串常量,以及newString这
个实例对象。如果abc这字符串常量存在,则只会创建一个对象。

引用类型有哪些

强引用

代码中普遍存在的赋值方式,比如new对象。只要强引用存在,被引用的对象绝对不会被垃圾回收器回收,即使内存不足,JVM 也会抛出OutOfMemoryError,而不会回收强引用指向的对象。

软引用

强度弱于强引用,用于描述「有用但非必需」的对象。

  • 当内存充足时,软引用指向的对象不会被回收;
  • 当内存不足(即将发生 OOM)时,垃圾回收器会优先回收软引用指向的对象,以释放内存。

弱引用

强度弱于软引用,用于描述「非必需」的对象。

无论内存是否充足,只要发生垃圾回收(GC),弱引用指向的对象都会被回收(一旦 GC 触发,立即回收)。

场景:适用于存储「临时关联」的对象,避免内存泄漏。例如:

  • ThreadLocal的内部实现中,Entry的 key 使用弱引用,防止线程销毁后 key 无法回收;
  • 缓存中不常用的临时数据,希望 GC 时自动清理。

虚引用

强度最弱的引用,又称「幽灵引用」或「幻影引用」,几乎不影响对象的生命周期。

  • 虚引用指向的对象随时可能被回收,且无法通过get()方法获取对象(调用get()始终返回null);
  • 唯一作用是:当对象被回收时,虚引用会被加入到关联的「引用队列」中,用于跟踪对象的回收状态

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

内存泄漏(Memory Leak)和内存溢出(Memory Overflow)是 JVM 内存管理中两个密切相关但本质不同的概念,前者是 “内存浪费” 的渐进过程,后者是 “内存耗尽” 的最终结果,具体区别和联系如下:

一、内存泄漏(Memory Leak)

定义:程序中已动态分配的内存(通常是堆内存中的对象),由于逻辑错误或设计缺陷,导致其不再被使用时,仍被错误地持有引用,无法被垃圾回收器(GC)回收,从而长期占用内存的现象。

简单说:“该回收的内存没被回收,变成了‘无效垃圾’占用空间”

典型场景:

  1. 静态集合未清理:静态集合(如static List<Object> list)持有对象引用,若只添加不删除,对象会一直被强引用,即使不再使用也无法回收。

  2. 监听器 / 回调未移除:注册的监听器(如 GUI 组件的事件监听器、网络连接的回调)若未在对象销毁前注销,会导致监听器关联的对象被长期引用。

  3. ThreadLocal 使用不当ThreadLocalEntry中,key 是弱引用,但 value 是强引用。若线程长期存活(如线程池核心线程),且未调用remove(),value 会被永久持有,导致内存泄漏。

  4. 资源未关闭:数据库连接、文件流、网络 Socket 等资源若未显式关闭,其底层对象(如ConnectionInputStream)可能被 JVM 持有引用,无法回收。

  5. 缓存未设置过期策略:缓存(如HashMap实现的本地缓存)若无限存储数据,且没有淘汰机制(如 LRU),会导致旧数据长期占用内存。

二、内存溢出(Memory Overflow,OOM)

定义:程序在申请内存时,JVM 无法为其分配足够的内存空间(如堆、栈、元空间等区域的剩余内存不足),导致 JVM 抛出OutOfMemoryError的现象。

简单说:“需要的内存超过了可用内存,申请不到内存了”

典型场景:

  1. 堆内存溢出:短时间内创建大量对象(如无限循环new Object()),或内存泄漏累积到一定程度,堆内存被耗尽。
  2. 元空间溢出:动态生成大量类(如 CGLib 代理、Groovy 脚本编译),且类加载器未被回收,元空间(存储类元数据)内存不足。
  3. 栈溢出:方法调用层级过深(如无限递归),栈帧数量超过栈容量,抛出StackOverflowError(本质是栈内存的溢出)。
  4. 直接内存溢出:NIO 的DirectByteBuffer分配过多,超过系统总内存或-XX:MaxDirectMemorySize限制。

栈溢出的情况怎么解决

从触发原因来看,最常见的场景是无限递归调用。因为Java方法调用时会在栈中创建栈帧(存储局部变量、操作数栈、方法返回地址等),每递归一次就会新增一个栈帧。如果递归没有正确的终止条件,栈帧会不断累积,最终超过虚拟机栈的最大容量,导致栈溢出。

另一种情况就是说单个方法的栈帧过大。如果一个方法定义了大量局部变量,或者局部变量占用内存过大(比如大数组),单个栈帧就会占用较多栈空间,可能在调用层级不深时就耗尽栈内存。

解决思路:

1.首先排查递归逻辑,查看是否存在无限递归调用或者递归层级过深的问题,添加正确的终止条件,或者减少递归深度。

2.调整栈内存的大小:通过JVM参数-Xss增大栈内存容量。但是这种方式要谨慎,占内存过大会导致线程可创建数量减少。

3.优化方法栈帧:减少方法内局部变量的数量,避免在方法中创建过大的对象和数组,讲过大对象放到堆里面,降低单个栈帧的内存占用。

堆溢出的情况怎么解决

堆溢出通常发生在程序持续创建对象且无法被垃圾回收器回收的场景下。

1.捕获内存快照:通过JVM参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof,让程序在发生OOM 时自动生成堆快照文件。

2.使用MAT或JProfiler或者阿里的Arthas等工具分析快照,重点看哪些对象占用了大量内存、是否存在内存泄漏。

常见的解决思路根据原因不同而不同:

如果是内存泄漏:比如静态集合无意识缓存大量对象、长生命周期对象持有短生命周期对象的引用。这时候需要梳理对象的引用链,找到未释放的根源。

如果是内存不足:即程序确实需要大量内存,但当前配置太小可以通过JVM调整参数 ,比如-Xms -Xmx

类加载和初始化

什么是类加载

类加载就是 JVM 把 .class 文件读进来,解析成 JVM 能理解的数据结构,并在内存中生成一个 Class 对象的过程。

之后:

  • 代码通过这个 Class 对象创建实例;
  • 反射、方法调用、字段访问等,都依赖这个类的元信息。

类加载器有哪些

类加载的核心是 ClassLoader,即类加载器。它负责根据类的全限定名(如 java.lang.String)找到并加载对应的 .class 文件。

img
  • Bootstrap ClassLoader(启动类加载器)

    ​ 这是最顶层的类加载器,他没有父加载器,负责去加载java的核心库,它使用c++编写,是jvm的一部分。启动类加载器无法被java程序直接引用。

  • Extension ClassLoader(扩展类加载器)

    它是java语言实现的,继承自ClassLoader类,负责加载java的扩展库,扩展类加载器由启动类加载器加载

  • Application ClassLoader(应用类加载器)

    默认加载 classpath 下的类;

    就是我们写的业务代码所在的加载器;

    sun.misc.Launcher$AppClassLoader 实现。

  • 自定义 ClassLoader

    开发者可以继承 ClassLoader,实现 findClass() 方法,
    自定义加载规则,比如:

    • 从网络加载类;
    • 从数据库或加密文件加载;
    • 热部署(如 Tomcat、Spring Boot DevTools)。

Java中双亲委派是什么?有啥用?

双亲委派机制(Parent Delegation Model) 是理解 Java 类加载体系的关键之一,它直接关系到类的唯一性、安全性,以及模块隔离。

当一个类加载器要加载某个类时,它不会自己立即动手,而是先把这个请求交给父加载器去处理;父加载器再往上层委托,直到到达最顶层的 Bootstrap ClassLoader;只有当上层加载器都找不到该类时,当前加载器才会自己去尝试加载。

假设我们调用:Class.forName("java.lang.String");

加载流程如下:

  1. 应用类加载器(AppClassLoader) 接收到加载请求;
  2. 它先把请求交给 扩展类加载器(ExtClassLoader)
  3. 扩展类加载器又将请求交给 启动类加载器(Bootstrap ClassLoader)
  4. 启动类加载器去 rt.jar(或模块)中查找 java.lang.String
  5. 找到了 → 返回 Class 对象;
    找不到 → 抛出 ClassNotFoundException,交回给子加载器;
  6. 若父加载器都加载失败,子加载器才会尝试自己去加载。

作用:

​ 保证核心类的安全性,防止用户自定义“假冒”核心类。

​ 避免类的重复加载,Java要求同一个类(同名、同包)只能被一个类加载器加载一次。双亲委派确保了同一类只在最顶层被加载一次。

类加载的流程是怎么样的

在 JVM 规范中,类加载(Class Loading) 包含以下 3 个大阶段(共 5 个具体步骤):

加载(Loading)
→ 链接(Linking)
→ 验证(Verification)
→ 准备(Preparation)
→ 解析(Resolution)
→ 初始化(Initialization)

加载

通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口

链接(Linking)

链接就是 JVM 把类加载进来后,让它变成一个可执行的类

验证(Verification)

确保类文件安全合法,不会破坏虚拟机。

验证的 4 个层面:

  • 文件格式验证:检查魔数(0xCAFEBABE)、版本号。
  • 元数据验证:比如类的父类是否存在、是否继承 final 类。
  • 字节码验证:指令流是否合法(不会越界、栈深度正确)。
  • 符号引用验证:常量池中的符号引用是否合法。

这是安全防线,防止恶意字节码攻击 JVM。

准备(Preparation)

为类的静态变量(类变量)分配内存,并赋默认值(不是初始化值)。

解析(Resolution)

把常量池中的“符号引用”替换成“直接引用”。

解析阶段:

  • JVM 把这些字符串(符号)解析成指针(内存地址),指向实际的方法或字段。
  • 有点像“从名字查地址”的过程。

解析可在运行时延迟进行(称为懒解析)。

初始化(Initialization)

真正执行类中的初始化逻辑(静态代码)

初始化的触发条件:

  1. 使用 new 创建类实例;
  2. 调用类的静态方法;
  3. 访问类的静态变量;
  4. 反射调用;
  5. 子类初始化时父类先初始化;
  6. 含有 main() 方法的类。

执行内容:

  1. 执行静态变量的赋值语句;
  2. 执行静态代码块(static {});
  3. 顺序遵循代码出现顺序
  4. 父类先于子类初始化。

对象的创建过程

类加载完毕后,就可以实例化对象。以 Person p = new Person(); 为例,JVM 做了以下几步。

  1. 检查类是否已经加载

    如果 Person 类未被加载或初始化,会先触发上面的类加载流程。

  2. 分配内存(在堆中)

    JVM 在 堆(Heap) 中为新对象分配所需的内存空间。大小取决于类中实例变量的数量与类型。

  3. 内存初始化(零值)

    分配到的内存先清零,确保对象的字段都有默认值:

    • 数值型:0;
    • 布尔型:false;
    • 引用型:null。

    此时对象结构已确定,但还未执行任何构造代码。

  4. 设置对象头(Object Header)

    JVM 在对象头中写入一些重要信息:

    • 类的元数据指针(即指向 Class 对象);
    • GC 标志、哈希码;
    • 锁状态(偏向锁、轻量级锁等)。

    这部分决定了对象能被识别、锁定、垃圾回收。

  5. 执行 <init> 构造函数

    这一步是真正初始化对象逻辑的地方。

    顺序如下:

    1. 调用父类构造函数;
    2. 按声明顺序给实例字段赋初始值;
    3. 执行子类构造函数体。

    生成的字节码会调用 <init> 方法(特殊方法名)。

  6. 返回对象引用

    构造完成后,返回堆中对象的引用(句柄或直接指针),保存在栈帧的局部变量表中。

JVM的垃圾回收机制

什么是垃圾回收(GC)?如何触发垃圾回收?

GC(Garbage Collection,垃圾回收)是自动管理程序内存的机制,它帮助程序员自动释放那些不再被程序所 使用的内存对象,从而避免内存泄漏等问题。

内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,就会触发垃圾回收

手动请求:虽然垃圾回收是自动的,开发者可以通过调用system.gc()Runtime.getRuntime.gd()建议JVM进行垃圾回收

JVM参数:启动java应用时可以通过JVM参数来调整垃圾回收的行为,比如-Xmx(最大堆大小)、-Xms(初始堆大小)

对象数量或者内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发

JVM 采用分代回收机制:不同代用不同策略,提高效率。于是就有了 Minor GC、Major GC、Full GC。

1. Minor GC(轻量级回收)

  • 目标:清理新生代
  • Eden 区的对象大多是“短命对象”。
  • GC 时,幸存的对象从 Eden → Survivor。
  • 若多次 Minor GC 仍存活,则晋升到老年代。
  • 成本:停顿时间短,频率高。

触发时机:

Eden 区空间不足时自动触发。


2. Major GC(老年代回收)

  • 目标:清理老年代
  • 老年代的对象多为“长期存活”或“晋升对象”。
  • 使用“标记-清除”或“标记-整理”算法,效率低。
  • 停顿时间明显长于 Minor GC。

触发时机:

老年代空间不足(无法存放新晋升对象)时触发。


3. Full GC(全量回收)

  • 目标:回收整个堆 + 元空间
  • 代价极高,Stop-The-World,应用线程全部暂停。
  • 通常是“无路可退”的最后手段。

判断垃圾的方法有哪些?

在Java中,判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数法和可达性分析算法。

引用计数法(Reference Counting):

原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。

可达性分析算法(Reachability Analysis):

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

GC Root 是什么:

GC Root 是 Java 垃圾回收(Garbage Collection, GC)中判断对象是否“存活” 的起点。
在 JVM 中,垃圾回收并不是简单地扫描所有对象,而是从一组特定的“根对象”出发,沿着引用链(Reference Chain)去搜索。
如果一个对象能被这些根对象直接或间接引用到,就认为它是可达的(reachable),因此不会被回收。

VM 中能作为 GC Root 的对象主要包括以下几类:

  1. 虚拟机栈(栈帧中的局部变量表)中的引用对象
    • 比如方法中定义的局部变量、方法参数、临时对象引用。
    • 当线程执行到某个方法时,这个方法栈帧里的变量就是活跃的。
  2. 方法区中类静态属性引用的对象
    • 比如 static 修饰的成员变量引用的对象。
    • 静态变量属于类而非实例,因此始终存在于 JVM 生命周期内。
  3. 方法区中常量引用的对象
    • 比如字符串常量池中的对象:String s = "abc";
    • "abc" 是在方法区中常量池引用的对象。
  4. 本地方法栈中 JNI(Native 方法)引用的对象
    • 即通过 native 方法调用的本地代码中使用到的对象引用。
    • JNI 全称是 Java Native Interface。
  5. 正在被同步锁(synchronized)持有的对象
    • 比如正在被锁住的对象 synchronized(lock) 中的 lock
    • JVM 会认为它仍然“被使用”,因此不能被 GC。
  6. JVM 自身持有的系统类加载器等特殊引用
    • 比如类加载器(ClassLoader)、线程对象(Thread)、异常对象等。
    • 这些对象是 JVM 自身运行所需的,不会轻易被 GC。

垃圾回收器

Serial 单线程;Parallel 求吞吐;CMS 追响应;G1 智能平衡;ZGC、Shenandoah 超低延迟。

  • Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器(复制算法):新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • ParallelScavenge收集器(复制算法):新生代并行收集器,追求高吞吐量,高效利用CPU。吞吐量=用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  • SerialOld收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本;
  • ParallelOld收集器(标记-整理算法):老年代并行收集器,吞吐量优先,ParallelScavenge收集器的老年代版本;
  • CMS(ConcurrentMarkSweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(GarbageFirst)收集器(标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代

JVM垃圾回收算法

自动识别不再被引用的对象,并安全高效地释放内存。

它是“怎么识别垃圾、怎么清理垃圾、怎么移动对象”的一系列策略。

算法名称 核心思想 优点 缺点 典型应用
1️⃣ 标记-清除(Mark-Sweep) 分两步:先标记所有存活对象,再清除未标记的 简单、无需移动对象 产生内存碎片,分配新对象效率低 老年代(Serial Old、Parallel Old)
2️⃣ 复制算法(Copying) 把活对象复制到另一块空闲区域,再清空原区域 无碎片,分配快 浪费一半内存空间 新生代(Minor GC)
3️⃣ 标记-整理(Mark-Compact) 标记存活对象后,将它们移动到堆的一端,再清除其他区域 无碎片、空间连续 需要对象移动,成本高 老年代(Serial Old、CMS Compact 阶段)
4️⃣ 分代收集(Generational Collection) 新生代用复制算法、老年代用标记整理 综合效率高 实现复杂 几乎所有现代 JVM(HotSpot 默认策略)
5️⃣ 区域化收集(Region-based,如 G1/ZGC) 把堆划分为许多 Region,每个 Region 独立回收 并行、可预测停顿时间 实现复杂,元数据管理成本高 G1、ZGC、Shenandoah

JVM调优

JVM 调优的目的

  1. 减少 GC 停顿时间(Stop The World)
    • 避免频繁 Full GC,降低对系统吞吐的影响。
  2. 提升吞吐量
    • 比如大数据处理、高并发接口,目标是 CPU 绝大部分时间都在跑业务逻辑,而不是收垃圾。
  3. 防止内存溢出(OOM)或内存泄漏
    • 程序在运行一段时间后稳定且不崩溃。

如何定位问题

常见的性能问题的类型:

类型 典型表现 解决方向
Full GC 频繁 吞吐量下降、系统卡顿 优化 GC 参数或对象生命周期
内存溢出 (OOM) 系统崩溃或接口 500 排查内存泄漏
响应慢 停顿时间长 优化 GC 算法或堆结构
CPU 飙高 GC 线程占用过多 减少对象创建或调整线程并发

可用的诊断工具:

  • jstat:监控 GC 次数与时间。
  • jmap / jhat:生成堆转储分析。
  • jconsole / VisualVM:实时监控堆、线程、类加载。
  • arthas:阿里开源神器,定位卡顿或内存泄漏。

JVM可调参数

参数 含义
-Xms / -Xmx 初始堆大小 / 最大堆大小
-Xmn 新生代大小
-XX:SurvivorRatio Eden 与 Survivor 比例(默认 8:1:1)
-XX:NewRatio 新生代与老年代比例
-XX:+UseConcMarkSweepGC 使用 CMS 垃圾回收器(JDK8 常用)
-XX:+UseG1GC 使用 G1 垃圾回收器(JDK11 推荐)
-XX:MaxGCPauseMillis 目标最大停顿时间
-XX:GCTimeRatio 吞吐量目标(GC 时间与非 GC 时间比)
-XX:+PrintGCDetails 输出 GC 详细日志
-Xloggc:/path/gc.log GC 日志路径