JVM篇
一.在JVM中,什么是程序计数器?
在 JVM(Java Virtual Machine) 中,程序计数器(Program Counter Register,简称 PC 寄存器) 是一块较小的内存空间,用于记录 当前线程所执行的字节码的行号指示器。
1. 程序计数器的作用
- JVM 的字节码解释器在工作时,需要依靠程序计数器来 确定下一条需要执行的字节码指令。
- 程序计数器存储的内容可以看作是 当前线程所执行的字节码的地址(行号)。
- 如果执行的是 本地方法(native 方法) ,那么程序计数器的值为 未定义(Undefined)。
2. 为什么需要程序计数器
- 多线程环境下 ,JVM 通过 线程切换 来实现并发执行。
- 每条线程都需要记录自己执行到哪里了,所以 程序计数器是线程私有的,每个线程都有独立的 PC 寄存器。
- 当线程切换回来时,程序计数器能帮助 JVM 知道该线程应该 从哪条指令继续执行。
3. 特点
- 占用内存非常小,几乎可以忽略。
- 是 JVM 规范中唯一一个没有规定任何 OOM(OutOfMemoryError)情况的内存区域。
- 属于线程私有(Thread-Private)内存。
二.你能详细给我介绍一下Java堆吗?
1. 什么是 Java 堆(作用)
Java 堆是 JVM 管理的 一块用于存放 Java 对象实例(以及数组)的运行时内存区域 。
它是 所有线程共享 的堆内存区(与线程私有的栈、程序计数器不同)。JVM 的垃圾回收器(GC)主要作用于堆:及时回收不再被引用的对象,防止内存泄漏/耗尽。
2. 堆的逻辑划分(世代/区域)
传统的"堆"按代(Generation):
-
Young(新生代)
- Eden(伊甸区)
- Survivor0(S0)/ Survivor1(S1)------两个幸存者区交换使用
新生代主要承载新创建的对象。大多数对象短命,会在这里被回收(Minor GC)。
-
Old / Tenured(老年代 / 年长代)
存放在多次 GC 后仍然存活、被晋升(promote)的对象。对老年代的回收通常更昂贵(Major/Full GC)。
注意:JDK 8 后的 Metaspace(方法区)已经移出堆(替代 PermGen)。Metaspace 存放类元数据,不属于堆空间。

三.什么是虚拟机栈? 垃圾回收机制是否涉及栈内存? 栈内存是越大越好吗 ?方法内的局部变量是否线程安全? 什么情况下会导致栈内存溢出?
1) 什么是虚拟机栈
- 虚拟机栈(JVM Stack)是 JVM 为每个 Java 线程创建的私有内存区域。
- 栈由若干**栈帧(StackFrame)**组成:每个方法调用对应一个栈帧,栈帧里保存方法的局部变量表、操作数栈、常量池引用和返回地址等。
- 线程结束时其虚拟机栈被回收。
- 举例 :线程 A 调用
foo()
→bar()
,会在栈中先后压入foo
、bar
的帧,bar
返回后其帧弹出。
2) 垃圾回收机制是否涉及栈内存
- GC 主要回收堆(Heap)上的对象,栈上的局部变量本身不被 GC。
- 但是,栈上的引用(局部变量指向的对象引用)会被当作 GC Roots,GC 会从这些根开始标记可达对象,从而间接影响回收。
- 举例 :方法中
Object o = new Object();
,只要o
仍在栈上可达,那个对象不会被回收;方法返回后o
不再可达,对象就可能被回收。
3) 栈内存是越大越好吗?
-
不是绝对越大越好,有利有弊:
- 增大
-Xss
可以支持更深的调用深度或更大的栈帧(例如深递归),减少StackOverflowError
风险。 - 但每个线程都占用这个栈空间,栈越大可同时支持的线程数越少,一个栈对应一个线程,栈的内存过大,可能导致系统无法创建更多线程或出现 OOM。
- 增大
-
建议:一般使用默认大小,只有在确切需要(深递归或特殊 native 调用)时才调大,或在大量线程场景下调小。
4) 方法内的局部变量是否线程安全
-
局部基本类型变量(如
int
)和局部引用变量本身是线程私有的 ,所以它们的存取不会被多个线程同时修改------局部变量本身是线程安全的。 -
但局部变量引用的对象可能是共享的,如果该对象被多个线程访问则可能不安全。
-
举例:
javavoid f() { int a = 0; // 线程安全 List<String> list = new ArrayList<>(); // 如果不把 list 发布到其他线程,则安全 sharedList.add("x"); // 如果 sharedList 是共享的,可能产生线程安全问题 }
-
若需要在不同线程间隔离数据,可使用
ThreadLocal<T>
。
5) 什么情况下会导致栈内存溢出(StackOverflow)
-
典型原因:无限/过深递归(最常见)、极深的调用链、或每帧占用太多栈空间(极少见于 Java,但可发生在 JNI/native 代码中)。
-
另外,创建大量线程(每个线程都占栈)也会因为总栈消耗过大而引发
OutOfMemoryError
或无法创建新线程。 -
错误表现 :
java.lang.StackOverflowError
(单线程栈溢出);大量线程耗尽内存可能出现OutOfMemoryError: unable to create new native thread
。 -
举例(递归导致):
javavoid recurse() { recurse(); } // 调用会很快抛出 StackOverflowError
四.能不能解释一下方法区,介绍一下运行时常量池?
1.方法区(Method Area)
定义
方法区是Java虚拟机(JVM)规范中定义的运行时数据区的一部分,用于存储已被虚拟机加载的类信息、常量、静态变量 等数据。它是线程共享的内存区域,所有线程都可以访问方法区中的数据。
方法区的特点
- 线程共享:方法区是所有线程共享的内存区域。
- 逻辑内存区 :方法区是JVM规范中的一个逻辑概念,具体实现依赖于JVM的实现方式。例如:
- JDK 1.7及之前 :方法区通过永久代(PermGen)实现,存储在Java堆的永久代中。
- JDK 1.8及之后 :方法区通过元空间 (Metaspace)实现,使用本地内存(Native Memory)存储类的元数据,与Java堆分离。
- 内存回收 :方法区的垃圾回收效率较低,主要回收废弃的类信息 和常量池中的无用常量。
- 动态调整:方法区的大小可以动态调整(如元空间的默认大小不受限制,但可以通过参数配置)。
方法区存储的内容
- 类信息 :
- 类的全限定名(如
java.lang.String
)。 - 类的父类、接口、修饰符(如
public
、abstract
)。 - 字段(属性)的名称、类型、修饰符。
- 方法的名称、参数、返回值、修饰符、字节码(方法体)。
- 类的全限定名(如
- 运行时常量池:存储编译期生成的字面量和符号引用(后文详细说明)。
- 静态变量 :被
static
修饰的类变量。
2.运行时常量池(Runtime Constant Pool)
定义
运行时常量池是方法区的一部分,用于存储类文件中的常量数据(如字面量和符号引用),并在运行时进行动态解析和扩展。它是每个类或接口的运行时数据,由JVM在加载类时从类文件的常量池解析而来。
运行时常量池的作用
- 存储字面量和符号引用 :
- 字面量 :如字符串(
"Hello"
)、整数(123
)、浮点数(3.14
)等。 - 符号引用 :类、字段、方法的符号名称(如
java/lang/Object.toString:()Ljava/lang/String;
)。
- 字面量 :如字符串(
- 支持动态链接:符号引用在运行时会被解析为直接引用(如内存地址)。
- 节省内存:相同的数据在常量池中只存储一份,避免重复。
运行时常量池的存储内容
- 字面量(Literals) :
- 字符串常量(如
"Hello"
)。 - 数值常量(如
int 42
、double 3.14
)。 final
常量(如static final int MAX = 100;
)。
- 字符串常量(如
- 符号引用(Symbolic References) :
- 类和接口的全限定名 :如
java/lang/String
。 - 字段的符号引用 :包含字段的类名、字段名、字段描述符(如
Ljava/lang/String;
)。 - 方法的符号引用 :包含方法的类名、方法名、参数类型和返回值类型(如
main([Ljava/lang/String;)V
)。
- 类和接口的全限定名 :如
- 动态生成的常量 :
- 通过
String.intern()
方法添加的字符串。 - 动态语言支持(如
invokeDynamic
指令生成的调用点)。
- 通过
运行时常量池的版本差异
- JDK 1.6及之前 :
- 运行时常量池和字符串常量池都位于永久代(PermGen)。
- 如果常量池过大,可能导致
OutOfMemoryError: PermGen space
。
- JDK 1.7及之后 :
- 字符串常量池 被移到Java堆中。
- 其他常量池数据(如符号引用)仍保留在方法区(元空间)。
- JDK 1.8及之后 :
- 方法区通过**元空间(Metaspace)**实现,使用本地内存,不再受Java堆大小的限制。
- 如果元空间内存不足,会抛出
OutOfMemoryError: Metaspace
。
示例:运行时常量池的作用
java
public class Example {
public static void main(String[] args) {
String str1 = "Hello"; // 字符串字面量,存储在运行时常量池
String str2 = "Hello"; // 直接引用常量池中的"Hello"
String str3 = new String("Hello"); // 堆中新建对象
System.out.println(str1 == str2); // true(常量池引用)
System.out.println(str1 == str3); // false(堆对象 vs 常量池)
}
}
str1
和str2
:都指向运行时常量池中的"Hello"
。str3
:通过new
创建的新对象,存储在堆中,与常量池无关。
常见问题
- 为什么需要运行时常量池?
- 节省内存:共享相同的数据(如重复的字符串、类名)。
- 支持动态链接:符号引用在运行时解析为直接引用,实现类、方法的动态绑定。
- 运行时常量池会导致内存溢出吗?
- 在 JDK 1.6 及之前,如果常量池过大,可能导致
PermGen space
溢出。 - 在 JDK 1.8 及之后,元空间使用本地内存,默认不限制大小,但仍需合理配置(如
-XX:MaxMetaspaceSize
)。
- 在 JDK 1.6 及之前,如果常量池过大,可能导致
总结
- 方法区是JVM的逻辑概念,存储类信息、常量、静态变量等,JDK 1.8之后通过元空间实现。
- 运行时常量池是方法区的一部分,存储编译期生成的字面量和符号引用,并在运行时动态解析。
- 版本差异:JDK 1.7之后字符串常量池移至堆中,JDK 1.8之后元空间取代永久代,解决了固定内存限制的问题。
五.你听说过直接内存吗,解释一下?
什么是直接内存
- 直接内存 指的是 JVM 通过
Unsafe
类 或者 NIO 中的ByteBuffer.allocateDirect()
方法,直接向操作系统申请的内存。 - 这块内存不受 JVM 堆大小参数(如
-Xmx
)限制,而是受到 本机物理内存 和-XX:MaxDirectMemorySize
参数限制。
为什么要有直接内存


因为传统 Java 堆内存的读写需要 先复制到 JVM 内存,再复制到操作系统内核内存 ,效率低。
而直接内存避免了这层拷贝:
- I/O 操作(比如网络传输、文件读写)可以直接操作这块内存,减少一次拷贝,提高性能。
典型场景:Java NIO 中的 零拷贝(Zero-Copy)
特点
- 分配和销毁成本比堆内存高。
- 访问速度通常比堆内存快,特别是在大数据量 I/O 场景下。
- 可能会导致 内存溢出(OutOfMemoryError: Direct buffer memory) ,即使堆内存还有空间,因为它不算在
-Xmx
里面。
举例
java
import java.nio.ByteBuffer;
public class DirectMemoryDemo {
public static void main(String[] args) {
// 分配 100MB 直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
System.out.println("分配了100MB直接内存");
}
}
如果运行时不加参数 -XX:MaxDirectMemorySize=200m
,但分配超过默认限制,就可能抛出:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
六.什么是类加载器,类加载器的种类有哪些?
什么是类加载器(ClassLoader)
- 类加载器 的作用:把字节码文件(.class)加载到 JVM 内存中,并生成对应的 Class 对象。
类加载器的种类
在 JVM 规范里,类加载器分为两大类:
- 启动类加载器(Bootstrap ClassLoader)
- 其他类加载器(继承自 ClassLoader 的加载器)
实际常见的类加载器有:
1. 启动类加载器(Bootstrap ClassLoader)
- 用 C++ 编写,属于 JVM 本地代码的一部分。
- 主要加载 JDK 核心类库。
2. 扩展类加载器(Extension ClassLoader)
- 由 Java 实现,父加载器是 Bootstrap。
- 加载 扩展目录(ext) 下的类(早期 JDK 是
jre/lib/ext
,后来 JDK9 之后改为模块化)。 - 负责加载一些非核心但又是 JDK 提供的扩展类库。
3. 应用类加载器(Application ClassLoader / System ClassLoader)
- 也叫 系统类加载器。
- 由 Java 实现,父加载器是 扩展类加载器。
- 负责加载 classpath 下的类(我们自己写的代码一般都是它加载的)。
4. 自定义类加载器
-
开发者可以继承
ClassLoader
,实现自定义加载逻辑。 -
常见应用场景:
- 热部署 / 插件机制(如 Tomcat、Spring Boot DevTools)。
- 字节码加密与解密(防止源码被反编译)。
类加载器的层次结构(双亲委派模型)

七.什么是双亲委派模型,JVM为什么要采用双亲委派机制?
1.什么是双亲委派模型(Parent Delegation Model)
定义:
在 JVM 中,类加载器在加载类时,并不是自己马上去尝试加载,而是把请求交给父类加载器去处理 ,父类加载器再交给更上层,直到 启动类加载器。
- 如果父类能完成加载,就直接返回结果。
- 如果父类不能完成加载(即找不到对应的类),再由当前类加载器自己尝试去加载。
2.双亲委派模型的好处
-
避免类的重复加载
- 保证同一个类只会由一个类加载器加载,避免不同类加载器重复加载相同类导致冲突。
- 例如:
java.lang.String
只能由 启动类加载器 加载,不会被用户自己写的类覆盖。
-
保证核心类库的安全性
- 如果用户自己写了一个
java.lang.String
类,放在classpath
下。 - 由于双亲委派,应用类加载器在加载
String
时,会先交给父加载器,最终由 启动类加载器 加载真正的String
。 - 避免了用户恶意替换核心类库。
- 如果用户自己写了一个
-
实现了类加载器的层级结构,模块化清晰
-
每个加载器只关注自己职责范围内的类:
- Bootstrap → JDK 核心类库
- Extension → 扩展类库
- Application → 应用程序类
-
既有分工,又能保证统一性。
-
八. 说一下类加载的执行过程?
📌 JVM 类加载的执行过程
1. 加载(Loading)
-
作用:将类的字节码文件(
.class
)读入内存,并创建一个Class
对象。 -
加载器:由 类加载器(ClassLoader) 完成,使用 双亲委派模型 来定位和加载类。
2. 链接(Linking)
分为三步:
-
验证(Verification)
- 确保字节码文件格式正确,不会危害 JVM 安全。
-
准备(Preparation)
-
为类的 静态变量(static 字段) 分配内存,并赋予默认值。
-
注意:这里只赋默认值 0 / null / false,不会执行任何赋值语句。
-
比如:
javapublic static int a = 10;
在 准备阶段 ,
a
的值是 0,不是 10。
-
-
解析(Resolution)
- 把常量池里的符号引用(字符串形式的类、方法、字段名)替换为 直接引用(内存地址)。
- 比如:
"java/lang/String"
→ 变成真正的String.class
对象引用。
3. 初始化(Initialization)
-
真正执行类变量的初始化代码,以及执行 静态代码块。
-
按照源代码中定义的顺序执行。
-
初始化子类前,必须先初始化父类,但 使用父类时,不会触发子类初始化。
-
比如:
javapublic class Test { static int a = 10; // ① static { a = 20; } // ② }
👉 初始化后
a = 20
,因为静态代码块在变量赋值之后执行。
✅ 总结:
类加载过程 = 加载 → 链接(验证、准备、解析) → 初始化,其中初始化阶段才会执行静态变量赋值和静态代码块。
九.在类加载中,准备阶段 和 初始化阶段 对不同类型变量(普通、static、final)的处理过程是什么?
1. 普通成员变量(非 static
)
- 准备阶段:不处理(因为普通成员变量属于对象实例,不属于类)。
- 初始化阶段:在对象实例化时,随着构造方法一起执行赋值。
java
public class Demo {
int a = 10; // 普通成员变量
}
👉 a
的赋值要等到 new Demo()
时才发生。
2. 静态变量(static
)
- 准备阶段 :分配内存并赋默认值(
0
、false
、null
)。 - 初始化阶段:执行显式赋值语句、静态代码块,按代码顺序赋值。
java
public class Demo {
static int a = 10; // ① 显式赋值
static { a = 20; } // ② 静态代码块
}
👉 准备阶段:a = 0
👉 初始化阶段:先执行 ① → a = 10
,再执行 ② → a = 20
3. final static
变量
-
情况 1:编译期常量(基本类型或
String
,值在编译期已确定)- 准备阶段:直接赋初始值(不会等到初始化阶段)。
- 因为编译器在编译时就把值放进了 常量池。
javapublic class Demo { public static final int A = 100; public static final String B = "Hello"; }
👉 在 准备阶段 ,
A = 100
,B = "Hello"
-
情况 2:运行期才能确定的值 (如
new
对象,方法返回值)- 准备阶段 :赋默认值(
0/null
)。 - 初始化阶段:执行赋值操作。
javapublic class Demo { public static final Integer C = Integer.valueOf(10); // 运行时决定 }
👉 准备阶段:
C = null
👉 初始化阶段:
C = Integer.valueOf(10)
- 准备阶段 :赋默认值(
4. 普通 final
变量(非静态)
- 属于对象实例变量,不在类加载阶段处理。
- 必须在构造方法或声明时赋值。
java
public class Demo {
final int x = 5; // 声明时赋值
final int y;
Demo(int y) { // 或者构造方法里赋值
this.y = y;
}
}
十.对象什么时候能被垃圾器回收?
1. 引用计数法(Reference Counting)
原理
-
给每个对象维护一个 引用计数器:
- 每当有一个地方引用它,计数器 +1。
- 引用失效时,计数器 -1。
-
当计数器 = 0 时,说明对象不可用,可以被回收。
优点
- 实现简单,效率高。
- 一旦计数为 0 就可以立即回收对象(不用等 GC 扫描)。
缺点
- 无法解决循环引用问题 :
两个对象互相引用,即使外部没有引用,它们的计数器也不是 0,无法被回收。
举例
java
class Node {
Node next;
}
public class Test {
public static void main(String[] args) {
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null; // 外部都断开了引用
// 但 a 和 b 互相引用,计数不为 0 → 无法回收
}
}
👉 因为这个问题,Java 没有采用引用计数法。
2. 可达性分析法(Reachability Analysis)
原理
- JVM 从一组称为 GC Roots 的对象出发,沿着引用链向下搜索。
- 如果一个对象与 GC Roots 没有任何引用链相连,就判定为不可达对象 → 可以被回收。
GC Roots 包括:
- 虚拟机栈中引用的对象(方法参数、局部变量等)
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈(JNI)引用的对象
GC Roots = JVM 里一切"根源性引用"的集合,比如线程、栈变量、静态变量、常量、JNI 引用等。
优点
- 可以有效避免循环引用问题。
- 更符合现代编程语言的需求,因此 Java 采用可达性分析来判断对象是否存活。
示例
java
class Parent {
static Parent p; // 静态变量(GC Root)
int[] arr = new int[1024];
}
public class Test {
public static void main(String[] args) {
Parent obj = new Parent(); // 局部变量 obj
obj = null; // 断开 obj
// obj 没有任何 GC Roots 引用 → 可被回收
// 但如果 Parent.p = obj; 那么对象就存活
}
}
十一.JVM垃圾回收算法有哪些?
JVM 的垃圾回收算法主要有以下几类,每一种算法在不同的场景下各有优缺点:
1. 标记-清除(Mark-Sweep)
-
过程:
- 从 GC Roots 开始标记所有可达对象。
- 清除未被标记的对象,回收内存。
-
优点:实现简单。
-
缺点:
- 会产生 内存碎片,不连续的内存影响大对象分配。
-
示例:假设堆内存像一张纸,标记可用的格子后,擦掉不用的内容,但留下了空洞。
2. 复制算法(Copying)
-
过程:
- 把内存分为两块(例如 Eden + Survivor0 / Survivor1)。
- 每次只用其中一块,当垃圾回收时,把存活对象复制到另一块,清空原来的区域。
-
优点:
- 没有碎片问题。
- 分配速度快(指针碰撞分配)。
-
缺点:
- 需要 双倍内存 空间。
-
示例:就像有两张纸,只写一张,用完后把重要的内容抄到另一张,再把旧纸扔掉。
3. 标记-整理(Mark-Compact)
-
过程:
- 标记存活对象。
- 将存活对象移动到内存的一端。
- 清理掉边界外的垃圾对象。
-
优点:解决了内存碎片问题。
-
缺点:移动对象需要额外开销,效率比复制算法低。
-
示例:像书架整理,把要保留的书一本本挪到左边,右边空出来。
✅ 总结
- 标记-清除:简单,但有碎片。
- 复制算法:无碎片,速度快,但浪费内存。
- 标记-整理:无碎片,但速度慢。
十二.说一下JVM中的分代回收?
一、JVM 堆的分代结构
JVM 堆通常分为:
-
新生代(Young Generation)
- 包含 Eden 区 、Survivor0 (S0) 、Survivor1 (S1)。
- 特点:对象朝生夕死,存活率低。
- 默认比例:
Eden : S0 : S1 = 8 : 1 : 1
。
-
老年代(Old Generation)
- 存放经过多次 GC 仍存活的对象。
- 特点:对象存活率高,内存大。
-
永久代(PermGen)/ 元空间(Metaspace,Java 8+)
- 存放类元数据(类结构、方法元信息等)。
- JDK8 之后用本地内存实现 Metaspace,不再在堆里。
二、回收过程
1. 新生代回收(Minor GC / Young GC)
-
触发条件:Eden 区满。
-
算法 :复制算法(Copying)。
-
过程:
- 存活对象从 Eden + 一个 Survivor 区,复制到另一个 Survivor 区。
- 清空 Eden 和用过的 Survivor 区。
- 如果 Survivor 放不下,部分对象会晋升到老年代(称为 晋升/提升)。
2. 老年代回收(Major GC / Old GC)
- 触发条件:老年代空间不足。
- 算法 :标记-清除(Mark-Sweep)或 标记-整理(Mark-Compact)。
- 特点:回收速度慢,可能会导致应用停顿时间长。
3. 整堆回收(Full GC)
-
触发条件:
- 老年代空间不足;
- 元空间不足;
- System.gc() 调用;
- 其他 GC 策略触发。
-
过程:回收新生代 + 老年代 + 元空间。
-
代价:非常昂贵,应尽量避免频繁 Full GC。
十三.说一下JVM有哪些垃圾和回收器?
一、JVM 垃圾回收的基本原理
- 垃圾回收器是负责回收不再使用的对象的组件。
- JVM 的垃圾回收主要关注 堆内存(Heap) 和 方法区(MetaSpace) 的回收。
- 回收策略基于 分代回收(Generational GC) 和 垃圾收集算法,而不同的垃圾回收器实现了不同的回收策略和算法。
二、JVM 常见的垃圾回收器
1. Serial GC(串行回收器)
-
特点:使用单线程进行垃圾回收,适用于单核 CPU 系统。
-
使用场景:低内存和小型应用。
-
回收过程:
- 新生代使用 复制算法(Copying)。
- 老年代使用 标记-清除 或 标记-整理(Mark-Compact)。
-
启动方式 :
-XX:+UseSerialGC
2. Parallel GC(并行回收器)
-
特点:多线程进行垃圾回收,通过并行回收提高吞吐量,适用于多核 CPU 系统。
-
使用场景:需要高吞吐量的应用。
-
回收过程:
- 新生代使用 复制算法(Copying)。
- 老年代使用 标记-整理(Mark-Compact)。
-
启动方式 :
-XX:+UseParallelGC
3. CMS GC(Concurrent Mark-Sweep, 并发标记-清除回收器)
-
特点:通过并发回收减少停顿时间,适用于低停顿应用。
-
使用场景:要求低延迟的应用。
-
回收过程:
- 新生代使用 复制算法(Copying)。
- 老年代使用 标记-清除 (Mark-Sweep)+ 并发清除。
-
启动方式 :
-XX:+UseConcMarkSweepGC
4. G1 GC(Garbage-First Garbage Collector)
-
特点:适合多核 CPU 和大堆内存的环境,目标是实现高效的垃圾回收,同时降低停顿时间。
-
使用场景:适合大内存、高并发、要求低延迟的应用。
-
回收过程:
- G1 会将堆分成多个 Region,每个 Region 由 G1 回收器动态选择回收。
- 新生代和老年代采用不同的回收策略,G1 会通过预测停顿时间来选择回收哪些区域。
-
启动方式 :
-XX:+UseG1GC
三、总结
- Serial GC:适用于小型应用,单线程,回收效率较低。
- Parallel GC:适用于多核 CPU,大型应用,追求高吞吐量。
- CMS GC:适用于低停顿、高并发应用,减少停顿时间。
- G1 GC:适合大内存、高并发应用,平衡吞吐量和停顿时间。
十四.请详细聊一下Java中的G1垃圾分类回收器?
1. G1的核心设计理念
G1的设计目标是通过灵活的分区管理 和优先级回收策略,解决传统垃圾回收器(如CMS)的痛点,例如:
- 内存碎片化:CMS的标记-清除算法可能导致碎片,无法分配大对象。
- 不可预测的停顿时间:CMS和Parallel Scavenge的停顿时间难以控制。
- 全堆回收的开销:传统回收器需要对整个堆进行回收,效率低下。
G1的核心思想:
- 分区(Region)管理:将堆划分为多个大小相等的独立区域(Region),每个区域可以动态分配给新生代或老年代。
- 增量式回收:每次只回收部分区域(Collection Set),避免全堆回收。
- 可预测的停顿时间 :通过启发式算法和用户设定的停顿目标(如
-XX:MaxGCPauseMillis
),控制GC停顿时间。 - 并发与并行结合:在标记和清理阶段充分利用多核CPU资源。
2. G1的内存模型
G1将堆划分为多个Region (默认大小1MB~32MB,可通过-XX:G1HeapRegionSize
调整),每个Region可以属于以下类型之一:
- Eden Region:存放新创建的对象(属于新生代)。
- Survivor Region:存放年轻代GC后存活的对象。
- Old Region:存放存活时间较长的对象(属于老年代)。
- Humongous Region :专门存储巨型对象(大小超过Region的一半)。
逻辑分代 vs 物理分代
- 传统分代(如CMS):新生代和老年代是物理上连续的内存区域。
- G1逻辑分代:新生代和老年代是逻辑上的概念,Region可以动态分配到任意分代中。
3. G1的工作原理
G1的回收过程分为四个主要阶段,通过增量式回收 和优先级列表实现高效垃圾回收:
1. 年轻代回收(Young GC)
- 触发条件:Eden区填满时触发。
- 过程 :
- 复制算法:将Eden和Survivor中的存活对象复制到新的Survivor区域。
- 对象晋升:如果Survivor区域不足,部分存活对象晋升到老年代。
- 清理空Region:回收不再使用的Eden和Survivor Region。
2. 并发标记(Concurrent Marking)
- 目标:标记老年代中的垃圾对象。
- 步骤 :
- 初始标记(STW):标记从根节点直接引用的对象。
- 并发标记:与用户线程并发执行,遍历老年代对象图。
- 最终标记(STW):处理并发标记期间的剩余任务。
- 筛选回收(Mixed GC):选择垃圾最多的Region进行回收(混合回收新生代和部分老年代)。
3. 混合回收(Mixed GC)
- 特点:在年轻代回收的基础上,回收部分老年代Region。
- 优先级策略:基于Region的垃圾比例和回收成本,优先回收垃圾最多的Region(Garbage-First名称的由来)。
4. 完全GC(Full GC)
- 触发条件:堆内存不足或G1无法回收足够空间时触发。
- 实现方式:使用单线程的标记-整理算法,停顿时间较长,需尽量避免。
4. G1的关键特性
特性 | 描述 |
---|---|
分区管理 | 堆被划分为多个Region,灵活分配到不同代,减少内存碎片。 |
可预测的停顿时间 | 用户通过-XX:MaxGCPauseMillis 设置目标停顿时间(默认200ms),G1会尽力满足。 |
并发与并行 | 并发标记阶段与用户线程并行运行;并行阶段(如Young GC)利用多核CPU加速。 |
空间整合 | 使用复制算法回收Region,避免内存碎片(对比CMS的标记-清除)。 |
动态调整 | Region的分配和回收策略动态调整,适应不同负载场景。 |
十五.强引用,软引用,弱引用,虚引用的区别是什么?
1. 强引用(Strong Reference)
-
定义 :最常见的引用类型,通过直接赋值(如
Object obj = new Object()
)创建。 -
回收时机 :只要存在强引用指向对象(GC Roots 能到达的对象),垃圾回收器永远不会回收该对象,即使内存不足。
-
使用场景 :
- 普通的业务对象(如业务实体、数据模型等)。
- 需要长期存活的对象(如缓存中的关键数据)。
-
示例 :
javaObject strongRef = new Object(); // 强引用 strongRef = null; // 显式置为null后,对象可被回收 System.gc(); // 建议JVM回收
2. 软引用(Soft Reference)
-
定义 :通过
SoftReference<T>
类创建,表示"有用但非必需"的对象。 -
回收时机 :
- 在内存充足时,不会被回收。
- 一旦内存不足(OOM)时,会被回收以释放内存。
-
使用场景 :
- 内存敏感的缓存(如图片缓存、缓存池),在内存不足时自动清理。
- 避免因缓存占用过多内存导致OOM。
-
示例 :
javaObject obj = new Object(); SoftReference<Object> softRef = new SoftReference<>(obj); obj = null; // 移除强引用 // 当内存不足时,softRef.get() 可能返回 null
3. 弱引用(Weak Reference)
-
定义 :通过
WeakReference<T>
类创建,表示"非必需"的对象。 -
回收时机 :
- 下一次垃圾回收时,只要没有强引用,就会被回收。
- 与内存是否充足无关。
-
使用场景 :
- 监听对象的回收(如监听某个对象是否被销毁)。
- 避免内存泄漏(如缓存中临时对象)。
-
示例 :
javaObject obj = new Object(); WeakReference<Object> weakRef = new WeakReference<>(obj); obj = null; // 移除强引用 // 下一次GC后,weakRef.get() 会返回 null
4. 虚引用(Phantom Reference)
- 定义 :通过
PhantomReference<T>
类创建,不能通过get()
方法获取对象。 - 回收时机 :
- 对象被回收后,虚引用才会被加入引用队列,由Reference Handler线程执行相关内存的清理操作。
- 使用场景 :
- 资源清理(如关闭文件句柄、释放本地资源)。
- 监控对象何时被回收 (需配合
ReferenceQueue
使用)。
Reference Handler线程的作用
- Reference Handler线程 是JVM启动时创建的一个守护线程 ,其核心职责是:
- 监控对象的回收状态 :当JVM的垃圾回收器(如CMS、G1等)回收对象时,会将对应的引用(软引用、弱引用、虚引用)加入一个全局的
pending
队列。 - 将引用加入对应的引用队列 :Reference Handler线程会从
pending
队列中取出引用,并根据其注册的ReferenceQueue
将其加入到程序可见的队列中。 - 触发后续处理逻辑:程序可以通过轮询或阻塞方式从引用队列中取出引用,进而执行资源清理操作(例如关闭文件句柄、释放本地资源等)。
- 监控对象的回收状态 :当JVM的垃圾回收器(如CMS、G1等)回收对象时,会将对应的引用(软引用、弱引用、虚引用)加入一个全局的
-
示例 :
javaObject obj = new Object(); ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue); obj = null; // 移除强引用 // 调用 System.gc() 后,phantomRef 会被加入 queue
对比总结
特性 | 强引用 | 软引用 | 弱引用 | 虚引用 |
---|---|---|---|---|
回收时机 | 永远不回收(除非显式置为 null ) |
内存不足时回收 | 下一次GC时回收 | 对象被回收后加入引用队列 |
是否可获取对象 | ✅ 通过 get() 获取 |
✅ 通过 get() 获取 |
✅ 通过 get() 获取 |
❌ 无法通过 get() 获取 |
是否需要引用队列 | ❌ 不需要 | ❌ 不需要 | ❌ 不需要 | ✅ 必须配合 ReferenceQueue |
典型用途 | 普通对象、关键数据 | 缓存(内存敏感) | 临时对象、监听回收 | 资源清理、对象回收监控 |
十六.JVM调优参数可以在哪设置参数值?
1. 命令行启动参数
-
适用场景 :直接通过命令行启动Java应用(如
java -jar app.jar
)。 -
设置方法 :
在启动命令中添加JVM参数,例如:bashjava -Xms256m -Xmx256m -XX:+UseG1GC -jar app.jar
-
参数类型 :
- 标准参数(
-X
):如-Xms
(初始堆大小)、-Xmx
(最大堆大小)。 - 非标准参数(
-XX
):如-XX:+UseG1GC
(启用G1垃圾回收器)。
- 标准参数(
2. IDE配置(如 IntelliJ IDEA)
- 适用场景:在开发环境中运行或调试Java应用。
- 设置方法 :
-
通过运行/调试配置 :
-
打开
Run/Debug Configurations
(快捷键Alt + Shift + F10
或菜单Run > Edit Configurations
)。 -
在
VM options
字段中输入参数,例如:-Xms256m -Xmx256m -XX:+PrintGCDetails
-
-
全局配置 :
- 在
File > Settings > Build, Execution, Deployment > Build Tools > [所选配置]
中设置全局的VM options
。
- 在
-
3. 中间件配置(如 Tomcat)
- 适用场景:部署在 Tomcat、WebLogic、WebSphere 等应用服务器中。
- 设置方法 :
- Tomcat :
-
编辑
setenv.sh
(Linux/Mac)或setenv.bat
(Windows)文件,添加参数到JAVA_OPTS
,例如:bashJAVA_OPTS="-Xms256m -Xmx256m -XX:+UseG1GC"
-
如果没有
setenv.sh
,可以手动创建或修改catalina.sh
中的JAVA_OPTS
。
-
- Tomcat :
4. 容器环境(如 Docker/Kubernetes)
- 适用场景:在容器化部署中(如 Docker、Kubernetes)。
- 设置方法 :
- Docker :
-
在
docker run
命令中通过-e
设置环境变量JAVA_OPTS
,例如:bashdocker run -e JAVA_OPTS="-Xms256m -Xmx256m" my-java-app
-
或在 Dockerfile 中指定
ENV JAVA_OPTS
。
-
- Kubernetes :
-
在 Deployment 或 Pod 的 YAML 文件中通过环境变量设置
JAVA_OPTS
,例如:yamlenv: - name: JAVA_OPTS value: "-Xms256m -Xmx256m -XX:+UseG1GC"
-
- Docker :
总结对比
场景 | 设置位置 | 典型参数示例 |
---|---|---|
命令行启动 | 启动命令 | -Xms256m -Xmx256m -XX:+UseG1GC |
IDE(如 IntelliJ) | 运行/调试配置 | -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError |
Tomcat | setenv.sh 或 setenv.bat |
JAVA_OPTS="-Xms256m -Xmx256m" |
Docker/Kubernetes | 环境变量 JAVA_OPTS |
-Xms256m -XX:+UseContainerSupport |
十七.常见的JVM调优的参数有哪些?
-
-Xms
:设置JVM初始堆内存大小(如-Xms2g
表示2GB)。 -
-Xmx
:设置JVM最大堆内存大小(如-Xmx4g
表示4GB)。- 推荐设置 :
-Xms
和-Xmx
设置为相同值,避免堆动态扩容导致的性能抖动。
- 推荐设置 :
-
-XX:SurvivorRatio
:设置Eden区与Survivor区的比例(如-XX:SurvivorRatio=8
表示Eden占年轻代的80%)。 -
G1回收器(Garbage First):
- 启用参数:
-XX:+UseG1GC
- 关键参数 :
-XX:MaxGCPauseMillis=200
:目标最大停顿时间。
- 启用参数:
-
-XX:MaxTenuringThreshold=N
:设置对象晋升到老年代的最大年龄(默认15)。
十八.说一下JVM的调优工具?
一、命令行工具(轻量级,适合线上快速排查)
-
jps(Java Virtual Machine Process Status Tool)
- 查看当前系统上运行的所有 Java 进程及其
pid
。
- 查看当前系统上运行的所有 Java 进程及其
-
jstat(JVM Statistics Monitoring Tool)
-
监控 类加载、垃圾回收、内存、JIT 编译 等信息。
-
示例:
bashjstat -gc <pid> 1000
每 1s 打印一次 GC 情况。
-
-
jmap(Memory Map Tool)
-
查看堆内存使用情况,导出堆转储(heap dump)。
-
示例:
bashjmap -heap <pid> jmap -dump:live,format=b,file=heap.hprof <pid>
-
-
jhat(JVM Heap Analysis Tool)
- 分析
jmap
生成的 heap dump 文件。
- 分析
-
jstack(Stack Trace Tool)
-
打印指定进程的线程快照,定位 死锁、死循环、线程阻塞 问题。
-
示例:
bashjstack <pid> > threadDump.txt
-
二、图形化工具(直观,适合长期监控和分析)
-
JConsole
- JDK 自带,基于 JMX,实时监控内存、线程、类加载、MBean 等。
- 缺点:性能一般,功能偏简单。
-
VisualVM
- 功能强大的分析工具,可以监控 CPU、内存、线程、GC,还能分析 heap dump。
- 可安装插件(如 BTrace)增强功能。
- 推荐作为调优首选工具。
十九.JVM内存泄漏的排查思路有哪些?
1️⃣ 导出内存快照(Heap Dump)
当怀疑内存泄漏(Heap 使用持续上涨,Full GC 频繁且效果不明显)时,可以先用 jmap
导出内存快照:
bash
# 导出堆快照文件(hprof 格式)
jmap -dump:live,format=b,file=heap.hprof <pid>
live
:只导出存活对象(减少无效数据)file=heap.hprof
:生成的快照文件<pid>
:Java 进程号,可以通过jps
查看
⚠️ 注意:jmap
dump 会造成 Stop The World,生产环境需要谨慎操作,最好在压力低时执行。
2️⃣ 使用 VisualVM 加载快照
打开 VisualVM → File
→ Load
→ 选择刚刚生成的 heap.hprof
文件,进入内存分析界面。
VisualVM 提供几个关键视角:
(1)Classes 视角
-
按类展示实例数量和占用内存大小。
-
排查思路:
- 看哪些类的实例数量异常大(比如
HashMap$Node
、byte[]
、String
等)。 - 判断是否符合业务预期(例如缓存对象是否被回收)。
- 看哪些类的实例数量异常大(比如
(2)Instances 视角
-
可以点进某个类,查看对象实例。
-
排查思路:
- 查看对象的生命周期是否合理。
- 比如某个 Session 对象明明用户退出后应该被销毁,但还存活在内存中。
(3)References 视角(引用链分析)
-
查看某个对象的 引用路径(Reference Chain)。
-
排查思路:
- 找出 GC Roots → 对象的保留链。
- 如果对象本应释放,却因为被某个 静态集合、缓存、ThreadLocal 引用而无法回收,就说明有内存泄漏。
3️⃣ 常见内存泄漏场景(结合 VisualVM 分析)
-
静态集合持有对象
- 例:
static List
或Map
没有清理,导致对象一直被引用。 - 在 VisualVM 的 Reference Chain 中,可以看到对象被某个
static
字段强引用。
- 例:
-
缓存未设置过期策略
- 使用
HashMap
或ConcurrentHashMap
缓存,但没清理过期数据。 - 在 VisualVM 中看到大量缓存对象,引用路径来自缓存类。
- 使用
-
Listener / Callback 未释放
- 注册的监听器没 remove,导致被引用。
- 在 VisualVM 中,实例的引用路径显示来源是某个 listener 列表。
-
ThreadLocal 泄漏
- ThreadLocal 使用不当(没有调用
remove()
),导致 value 不能被回收。 - 在快照中可看到
ThreadLocalMap.Entry
引用了大量对象。
- ThreadLocal 使用不当(没有调用
-
数据库连接 / IO 资源未关闭
- 在快照中可能会看到大量的
Socket
、FileInputStream
对象。
- 在快照中可能会看到大量的
二十.CPU飙高的排查方案和思路是什么?
假设你发现某个 Java 进程 CPU 很高,你想找出是哪个线程导致的:
✅ 步骤 1:用 top -Hp <pid>
找出高 CPU 的线程 ID(十进制)
bash
top -Hp 12345
输出中看到某个线程 PID 是 12346
,占用 98% CPU。
✅ 步骤 2:将线程 ID 转为 16 进制
bash
printf "%x\n" 12346
# 输出:303a
✅ 步骤 3:用 jstack
+ grep
查找该线程的堆栈
bash
jstack 12345 | grep -A 30 303a # 查看该线程的调用栈
注意:我们搜索的是
303a
(16进制),因为jstack
中的nid
是 16进制格式。
✅ 输出示例:
bash
"main" #1 prio=5 os_prio=0 tid=0x00007f8c8000a000 nid=0x303a runnable [0x00007f8c8556d000]
java.lang.Thread.State: RUNNABLE
at com.example.Calculator.compute(Calculator.java:45)
at com.example.Service.handleRequest(Service.java:30)
at com.example.ApiController.process(ApiController.java:20)
at com.example.Main.main(Main.java:10)
这就定位到了:是 main
线程在执行 compute()
方法,可能是一个死循环或密集计算,导致 CPU 占用过高。
🔍 关键概念解释
名称 | 说明 |
---|---|
pid |
Java 进程的进程 ID(Process ID) |
tid |
Java 线程对象 ID(java.lang.Thread 的 ID,jstack 中 tid=... ) |
nid |
Native Thread ID ,操作系统线程 ID,16进制,jstack 中 nid=0xabc |
os_prio |
操作系统线程优先级 |
runnable / TIMED_WAITING / BLOCKED |
线程状态,反映当前线程在做什么 |