Skip to main content

Java 虚拟机面试题集

总题数: 51道 | 重点领域: JVM 内存、垃圾回收、类加载 | 难度分布: 中级到高级

本文档整理了 Java 虚拟机的完整51道面试题目,涵盖内存模型、垃圾回收、类加载、性能调优等各个方面。


面试题目列表

1. Java 中有哪些垃圾回收算法?

答案:

Java中主要有四种垃圾回收算法,各有特点和适用场景。

1. 标记-清除算法(Mark-Sweep)

原理:

  • 标记阶段:标记所有需要回收的对象
  • 清除阶段:回收被标记的对象

优点:

  • 实现简单

缺点:

  • 产生内存碎片
  • 效率不高(两次遍历)

2. 标记-复制算法(Mark-Copy)

原理:

  • 将内存分为两块
  • 只使用其中一块
  • GC时将存活对象复制到另一块
  • 清空当前块

优点:

  • 无内存碎片
  • 效率高(只遍历存活对象)

缺点:

  • 内存利用率低(只用一半)
  • 存活对象多时效率降低

应用:

  • 新生代(Eden + Survivor)

3. 标记-整理算法(Mark-Compact)

原理:

  • 标记阶段:标记存活对象
  • 整理阶段:将存活对象移动到内存一端
  • 清除阶段:清理边界外的内存

优点:

  • 无内存碎片
  • 内存利用率高

缺点:

  • 需要移动对象,效率较低
  • 需要暂停应用(STW)

应用:

  • 老年代

4. 分代收集算法(Generational Collection)

原理:

  • 根据对象存活周期划分区域
  • 新生代:使用复制算法
  • 老年代:使用标记-清除或标记-整理

依据:

  • 弱分代假说:大部分对象朝生夕灭
  • 强分代假说:熬过多次GC的对象难以消亡

优点:

  • 结合各算法优势
  • 针对性优化

2. JVM 的 TLAB(Thread-Local Allocation Buffer)是什么?

答案:

TLAB是JVM为每个线程在Eden区分配的私有缓冲区,用于提高对象分配效率。

1. 为什么需要TLAB

问题:

  • 多线程并发分配对象需要同步
  • 频繁加锁影响性能

解决方案:

  • 为每个线程预分配一块内存
  • 线程在自己的TLAB中分配对象
  • 无需同步,提高效率

2. TLAB工作原理

java
1// 对象分配流程
2public Object allocate(int size) {
3 // 1. 尝试在TLAB中分配
4 if (tlab.canAllocate(size)) {
5 return tlab.allocate(size);
6 }
7
8 // 2. TLAB空间不足,申请新的TLAB
9 if (size < TLAB_SIZE) {
10 tlab = allocateNewTLAB();
11 return tlab.allocate(size);
12 }
13
14 // 3. 对象太大,直接在Eden区分配(需要加锁)
15 return allocateInEden(size);
16}

3. TLAB特性

大小:

  • 默认占Eden区的1%
  • 可通过-XX:TLABSize设置

生命周期:

  • 线程创建时分配
  • TLAB用完后重新分配
  • 线程结束时回收

4. 相关JVM参数

bash
1# 启用TLAB(默认开启)
2-XX:+UseTLAB
3
4# 设置TLAB大小
5-XX:TLABSize=256k
6
7# TLAB占Eden区的比例
8-XX:TLABWasteTargetPercent=1
9
10# 打印TLAB信息
11-XX:+PrintTLAB

5. 优势

  • 无锁分配:避免同步开销
  • 减少碎片:连续分配
  • 提高性能:分配速度快

3. Java 是如何实现跨平台的?

答案:

Java通过"一次编译,到处运行"的机制实现跨平台。

1. 核心机制

编译过程:

1Java源代码(.java) -> 编译器(javac) -> 字节码(.class)

执行过程:

1字节码(.class) -> JVM -> 机器码 -> 操作系统

2. 关键组件

字节码(Bytecode):

  • 平台无关的中间代码
  • JVM规范定义的指令集
  • 所有平台的JVM都能识别

Java虚拟机(JVM):

  • 平台相关的虚拟机实现
  • 负责将字节码翻译成机器码
  • 不同平台有不同的JVM实现

3. 实现原理

java
1// 示例代码
2public class Hello {
3 public static void main(String[] args) {
4 System.out.println("Hello World");
5 }
6}
7
8// 编译后的字节码(部分)
9public static void main(java.lang.String[]);
10 Code:
11 0: getstatic #2 // Field java/lang/System.out
12 3: ldc #3 // String Hello World
13 5: invokevirtual #4 // Method java/io/PrintStream.println
14 8: return

4. 跨平台架构

1┌─────────────────────────────────────┐
2│ Java Application │
3├─────────────────────────────────────┤
4│ Java API (rt.jar) │
5├─────────────────────────────────────┤
6│ JVM │
7├──────────┬──────────┬───────────────┤
8│ Windows │ Linux │ macOS │
9└──────────┴──────────┴───────────────┘

5. 平台差异处理

JNI(Java Native Interface):

java
1// 调用本地方法
2public class NativeDemo {
3 // 声明本地方法
4 public native void nativeMethod();
5
6 static {
7 // 加载平台相关的动态库
8 System.loadLibrary("native");
9 }
10}

6. 优缺点

优点:

  • 一次编译,到处运行
  • 降低开发成本
  • 便于维护

缺点:

  • 性能略低于原生代码
  • 依赖JVM环境
  • 启动速度较慢

4. JVM 由哪些部分组成?

答案:

JVM主要由类加载子系统、运行时数据区、执行引擎和本地接口四部分组成。

1. 整体架构

1┌─────────────────────────────────────────┐
2│ Class Loader Subsystem │
3├─────────────────────────────────────────┤
4│ Runtime Data Areas │
5│ ┌──────────┬──────────┬──────────┐ │
6│ │ Method │ Heap │ Stacks │ │
7│ │ Area │ │ │ │
8│ └──────────┴──────────┴──────────┘ │
9├─────────────────────────────────────────┤
10│ Execution Engine │
11│ ┌──────────┬──────────┬──────────┐ │
12│ │Interpreter│ JIT │ GC │ │
13│ └──────────┴──────────┴──────────┘ │
14├─────────────────────────────────────────┤
15│ Native Method Interface │
16└─────────────────────────────────────────┘

2. 类加载子系统(Class Loader Subsystem)

功能:

  • 加载.class文件
  • 链接(验证、准备、解析)
  • 初始化

组成:

  • Bootstrap ClassLoader(启动类加载器)
  • Extension ClassLoader(扩展类加载器)
  • Application ClassLoader(应用类加载器)

3. 运行时数据区(Runtime Data Areas)

线程共享区域:

方法区(Method Area):

  • 存储类信息、常量、静态变量
  • JDK 8后改为元空间(Metaspace)

堆(Heap):

  • 存储对象实例
  • 垃圾回收的主要区域
  • 分为新生代和老年代

线程私有区域:

程序计数器(PC Register):

  • 记录当前线程执行的字节码行号
  • 线程切换后能恢复到正确位置

虚拟机栈(VM Stack):

  • 存储局部变量、操作数栈、方法出口
  • 每个方法对应一个栈帧

本地方法栈(Native Method Stack):

  • 为本地方法服务
  • 类似虚拟机栈

4. 执行引擎(Execution Engine)

解释器(Interpreter):

  • 逐行解释执行字节码
  • 启动快,执行慢

即时编译器(JIT Compiler):

  • 将热点代码编译成机器码
  • 启动慢,执行快
  • C1编译器(Client)和C2编译器(Server)

垃圾回收器(Garbage Collector):

  • 自动回收不再使用的对象
  • 多种GC算法和收集器

5. 本地接口(Native Interface)

JNI(Java Native Interface):

  • 调用本地方法
  • 与操作系统交互

本地方法库(Native Method Libraries):

  • C/C++编写的库
  • 提供底层功能

6. 内存分布示例

java
1public class MemoryDemo {
2 // 类信息存储在方法区
3 private static int staticVar = 100; // 静态变量在方法区
4
5 public void method() {
6 int localVar = 10; // 局部变量在栈
7 Object obj = new Object(); // 对象在堆,引用在栈
8
9 // 字符串常量在字符串常量池(堆中)
10 String str = "Hello";
11 }
12}

5. 编译执行与解释执行的区别是什么?JVM 使用哪种方式?

答案:

JVM采用混合模式,结合解释执行和编译执行的优势。

1. 解释执行(Interpretation)

特点:

  • 逐行翻译字节码为机器码
  • 边解释边执行
  • 不生成目标代码

优点:

  • 启动快
  • 内存占用少
  • 跨平台性好

缺点:

  • 执行速度慢
  • 重复代码重复解释

2. 编译执行(Compilation)

特点:

  • 一次性将代码编译成机器码
  • 生成可执行文件
  • 直接执行机器码

优点:

  • 执行速度快
  • 优化充分

缺点:

  • 启动慢
  • 内存占用大
  • 平台相关

3. JVM的混合模式

分层编译(Tiered Compilation):

1Level 0: 解释执行
2
3Level 1: C1编译(简单优化)
4
5Level 2: C1编译(带profiling)
6
7Level 3: C1编译(完全优化)
8
9Level 4: C2编译(激进优化)

工作流程:

java
1// 1. 程序启动,解释执行
2public void method() {
3 // 解释器执行
4}
5
6// 2. 方法被频繁调用,触发JIT编译
7// 调用次数达到阈值(默认10000次)
8if (invocationCount > CompileThreshold) {
9 // C1编译器编译(快速编译)
10 compileWithC1();
11}
12
13// 3. 继续频繁调用,触发C2编译
14if (invocationCount > TierThreshold) {
15 // C2编译器编译(深度优化)
16 compileWithC2();
17}

4. 热点代码检测

方法调用计数器:

  • 统计方法调用次数
  • 达到阈值触发编译

回边计数器:

  • 统计循环执行次数
  • 检测循环热点

5. JVM参数配置

bash
1# 只使用解释器
2-Xint
3
4# 只使用编译器(需要预热)
5-Xcomp
6
7# 混合模式(默认)
8-Xmixed
9
10# 设置编译阈值
11-XX:CompileThreshold=10000
12
13# 启用分层编译(默认开启)
14-XX:+TieredCompilation
15
16# 打印编译信息
17-XX:+PrintCompilation

6. 性能对比

特性解释执行编译执行混合模式
启动速度中等
执行速度
内存占用中等
优化程度
适用场景短期运行长期运行通用

7. 实际应用

java
1// 示例:热点代码
2public class HotSpotDemo {
3 public static void main(String[] args) {
4 // 初始阶段:解释执行
5 for (int i = 0; i < 15000; i++) {
6 compute(i); // 前10000次解释执行
7 // 后5000次编译执行
8 }
9 }
10
11 // 热点方法
12 public static int compute(int n) {
13 return n * n + n;
14 }
15}

总结: JVM使用混合模式,初期解释执行保证快速启动,运行中将热点代码编译优化,兼顾启动速度和执行效率。

6. JVM 方法区是否会出现内存溢出?

答案:

会出现内存溢出,方法区(JDK 8后为元空间)也会发生OOM。

1. 方法区存储内容

  • 类的元数据信息
  • 常量池
  • 静态变量
  • JIT编译后的代码

2. 内存溢出场景

场景1:加载大量类

java
1// 动态生成大量类导致OOM
2public class MethodAreaOOM {
3 public static void main(String[] args) {
4 while (true) {
5 Enhancer enhancer = new Enhancer();
6 enhancer.setSuperclass(Object.class);
7 enhancer.setUseCache(false);
8 enhancer.setCallback(new MethodInterceptor() {
9 public Object intercept(Object obj, Method method,
10 Object[] args, MethodProxy proxy) {
11 return proxy.invokeSuper(obj, args);
12 }
13 });
14 enhancer.create(); // 动态生成类
15 }
16 }
17}
18// 错误:java.lang.OutOfMemoryError: Metaspace

场景2:大量使用反射

java
1// 反射生成类
2ClassPool pool = ClassPool.getDefault();
3for (int i = 0; i < 100000; i++) {
4 CtClass cc = pool.makeClass("com.example.Class" + i);
5 cc.toClass(); // 加载到方法区
6}

场景3:JSP页面过多

  • 每个JSP编译成一个类
  • 大量JSP导致方法区溢出

3. JDK版本差异

JDK 7及之前(永久代):

bash
1# 设置永久代大小
2-XX:PermSize=64m
3-XX:MaxPermSize=256m
4
5# 错误信息
6java.lang.OutOfMemoryError: PermGen space

JDK 8及之后(元空间):

bash
1# 设置元空间大小
2-XX:MetaspaceSize=128m
3-XX:MaxMetaspaceSize=512m
4
5# 错误信息
6java.lang.OutOfMemoryError: Metaspace

4. 元空间优势

  • 使用本地内存,不受堆大小限制
  • 自动扩展,默认无上限
  • 减少Full GC频率

5. 预防措施

bash
1# 合理设置元空间大小
2-XX:MetaspaceSize=256m
3-XX:MaxMetaspaceSize=512m
4
5# 监控元空间使用
6-XX:+TraceClassLoading
7-XX:+TraceClassUnloading

7. Java 中堆和栈的区别是什么?

答案:

堆和栈是JVM内存中两个重要的区域,用途和特性完全不同。

1. 核心区别对比

特性堆(Heap)栈(Stack)
作用域线程共享线程私有
存储内容对象实例、数组局部变量、方法调用
生命周期对象创建到GC回收方法调用到返回
大小较大(GB级)较小(MB级)
分配方式动态分配连续分配
回收方式GC自动回收自动弹出
异常OutOfMemoryErrorStackOverflowError
速度较慢很快

2. 内存结构

堆结构:

1Heap
2├── Young Generation (新生代)
3│ ├── Eden (伊甸区)
4│ ├── Survivor 0 (S0)
5│ └── Survivor 1 (S1)
6└── Old Generation (老年代)

栈结构:

1Stack
2├── Stack Frame 1 (栈帧1)
3│ ├── 局部变量表
4│ ├── 操作数栈
5│ ├── 动态链接
6│ └── 返回地址
7├── Stack Frame 2 (栈帧2)
8└── ...

3. 代码示例

java
1public class HeapStackDemo {
2 // 静态变量在方法区
3 private static int staticVar = 100;
4
5 // 实例变量在堆
6 private int instanceVar = 200;
7
8 public void method(int param) {
9 // param在栈(局部变量表)
10 int localVar = 10; // 栈
11
12 // obj引用在栈,对象在堆
13 Object obj = new Object();
14
15 // 数组引用在栈,数组对象在堆
16 int[] arr = new int[10];
17
18 // 字符串常量池在堆
19 String str = "Hello";
20 }
21}

4. 内存分配示例

java
1public void test() {
2 // 1. 基本类型在栈
3 int a = 10;
4 double b = 3.14;
5
6 // 2. 对象引用在栈,对象在堆
7 Person p = new Person();
8
9 // 3. 数组在堆
10 int[] arr = new int[100];
11
12 // 4. 包装类对象在堆
13 Integer i = new Integer(10);
14}

5. 异常情况

堆溢出(OutOfMemoryError):

java
1// 不断创建对象
2List<byte[]> list = new ArrayList<>();
3while (true) {
4 list.add(new byte[1024 * 1024]); // 1MB
5}
6// java.lang.OutOfMemoryError: Java heap space

栈溢出(StackOverflowError):

java
1// 无限递归
2public void recursion() {
3 recursion();
4}
5// java.lang.StackOverflowError

6. JVM参数配置

bash
1# 堆配置
2-Xms2g # 初始堆大小
3-Xmx4g # 最大堆大小
4-Xmn1g # 新生代大小
5
6# 栈配置
7-Xss1m # 每个线程栈大小

8. 什么是 Java 中的直接内存(堆外内存)?

答案:

直接内存是JVM堆外的内存,不受JVM堆大小限制,主要用于NIO操作。

1. 什么是直接内存

定义:

  • 操作系统的本地内存
  • 不在JVM堆中
  • 通过DirectByteBuffer访问

2. 使用场景

NIO操作:

java
1// 分配直接内存
2ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
3
4// 传统堆内存
5ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

文件IO:

java
1// 使用直接内存进行文件读写
2FileChannel channel = new RandomAccessFile("file.txt", "rw").getChannel();
3ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
4
5// 读取
6channel.read(buffer);
7
8// 写入
9buffer.flip();
10channel.write(buffer);

3. 直接内存优势

零拷贝:

1传统IO:
2磁盘 -> 内核缓冲区 -> JVM堆 -> 内核缓冲区 -> Socket
3(4次拷贝)
4
5直接内存:
6磁盘 -> 内核缓冲区 -> 直接内存 -> Socket
7(2次拷贝)

减少GC压力:

  • 不在堆中,不参与GC
  • 适合大数据量操作

4. 直接内存管理

分配:

java
1public class DirectMemoryDemo {
2 public static void main(String[] args) {
3 // 分配100MB直接内存
4 ByteBuffer buffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
5
6 // 使用
7 buffer.putInt(100);
8
9 // 释放(通过GC间接释放)
10 buffer = null;
11 System.gc();
12 }
13}

手动释放:

java
1// 使用Unsafe释放
2public static void freeDirectBuffer(ByteBuffer buffer) {
3 if (buffer instanceof DirectBuffer) {
4 ((DirectBuffer) buffer).cleaner().clean();
5 }
6}

5. 内存溢出

java
1// 直接内存溢出
2List<ByteBuffer> list = new ArrayList<>();
3while (true) {
4 ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
5 list.add(buffer);
6}
7// java.lang.OutOfMemoryError: Direct buffer memory

6. JVM参数配置

bash
1# 设置直接内存最大值
2-XX:MaxDirectMemorySize=512m
3
4# 不设置则默认等于-Xmx

7. 监控直接内存

java
1// 获取直接内存使用情况
2import sun.misc.SharedSecrets;
3
4long maxDirectMemory = sun.misc.VM.maxDirectMemory();
5long usedDirectMemory = SharedSecrets.getJavaNioAccess()
6 .getDirectBufferPool().getMemoryUsed();
7
8System.out.println("最大直接内存: " + maxDirectMemory);
9System.out.println("已用直接内存: " + usedDirectMemory);

8. 使用建议

适用场景:

  • 大文件IO
  • 网络通信
  • 频繁IO操作

注意事项:

  • 分配和释放成本高
  • 不受GC管理,需手动释放
  • 可能导致内存泄漏

9. 什么是 Java 中的常量池?

答案:

Java中有三种常量池:Class文件常量池、运行时常量池和字符串常量池。

1. Class文件常量池

位置:

  • .class文件中
  • 编译期生成

内容:

  • 字面量(字符串、final常量)
  • 符号引用(类、方法、字段)

查看:

bash
1# 查看class文件常量池
2javap -v ClassName.class

2. 运行时常量池

位置:

  • JDK 7之前:方法区(永久代)
  • JDK 7及之后:堆中

特点:

  • 类加载时从Class文件常量池加载
  • 动态性(可运行期添加)

3. 字符串常量池

位置:

  • JDK 7之前:永久代
  • JDK 7及之后:堆中

作用:

  • 避免重复创建字符串
  • 节省内存

4. 字符串常量池详解

示例1:字面量

java
1String s1 = "hello"; // 在常量池创建
2String s2 = "hello"; // 直接引用常量池
3System.out.println(s1 == s2); // true

示例2:new String()

java
1String s1 = new String("hello"); // 堆中创建对象
2String s2 = "hello"; // 常量池
3System.out.println(s1 == s2); // false
4
5String s3 = s1.intern(); // 返回常量池引用
6System.out.println(s2 == s3); // true

示例3:字符串拼接

java
1// 编译期确定,放入常量池
2String s1 = "hello" + "world";
3String s2 = "helloworld";
4System.out.println(s1 == s2); // true
5
6// 运行期确定,在堆中创建
7String s3 = "hello";
8String s4 = s3 + "world";
9String s5 = "helloworld";
10System.out.println(s4 == s5); // false

5. intern()方法

JDK 6:

java
1String s1 = new String("hello");
2String s2 = s1.intern();
3String s3 = "hello";
4System.out.println(s2 == s3); // true
5// intern()将字符串复制到永久代常量池

JDK 7+:

java
1String s1 = new String("he") + new String("llo");
2String s2 = s1.intern();
3String s3 = "hello";
4System.out.println(s1 == s3); // true
5// intern()将堆中字符串引用放入常量池

6. 常量池大小配置

bash
1# JDK 6/7 设置字符串常量池大小
2-XX:StringTableSize=1000003
3
4# JDK 8+ 默认60013
5-XX:StringTableSize=1000003

7. 实际应用

优化内存:

java
1// 大量重复字符串
2List<String> list = new ArrayList<>();
3for (int i = 0; i < 10000; i++) {
4 // 使用intern()减少内存占用
5 String s = new String("重复字符串").intern();
6 list.add(s);
7}

注意事项:

  • intern()有性能开销
  • 常量池大小有限
  • 避免intern()大量不同字符串

10. 你了解 Java 的类加载器吗?

答案:

类加载器负责将.class文件加载到JVM中,Java采用双亲委派模型。

1. 类加载器层次

1Bootstrap ClassLoader (启动类加载器)
2
3Extension ClassLoader (扩展类加载器)
4
5Application ClassLoader (应用类加载器)
6
7Custom ClassLoader (自定义类加载器)

2. 三种类加载器

Bootstrap ClassLoader:

  • C++实现
  • 加载核心类库(rt.jar)
  • 路径:$JAVA_HOME/jre/lib

Extension ClassLoader:

  • Java实现
  • 加载扩展类库
  • 路径:$JAVA_HOME/jre/lib/ext

Application ClassLoader:

  • Java实现
  • 加载应用类路径(classpath)
  • 默认的类加载器

3. 双亲委派模型

工作流程:

java
1public Class<?> loadClass(String name) {
2 // 1. 检查类是否已加载
3 Class<?> c = findLoadedClass(name);
4 if (c == null) {
5 try {
6 // 2. 委派给父加载器
7 if (parent != null) {
8 c = parent.loadClass(name);
9 } else {
10 // 3. 父加载器为null,委派给Bootstrap
11 c = findBootstrapClassOrNull(name);
12 }
13 } catch (ClassNotFoundException e) {
14 // 父加载器无法加载
15 }
16
17 if (c == null) {
18 // 4. 父加载器无法加载,自己加载
19 c = findClass(name);
20 }
21 }
22 return c;
23}

4. 双亲委派优势

避免重复加载:

  • 父加载器已加载,子加载器不再加载

安全性:

java
1// 自定义java.lang.String会被拒绝
2// 因为Bootstrap已加载核心String类
3public class String {
4 // 这个类不会被加载
5}

5. 自定义类加载器

java
1public class MyClassLoader extends ClassLoader {
2
3 private String classPath;
4
5 public MyClassLoader(String classPath) {
6 this.classPath = classPath;
7 }
8
9 @Override
10 protected Class<?> findClass(String name) throws ClassNotFoundException {
11 try {
12 // 读取class文件
13 byte[] classData = loadClassData(name);
14 if (classData == null) {
15 throw new ClassNotFoundException();
16 }
17 // 转换为Class对象
18 return defineClass(name, classData, 0, classData.length);
19 } catch (IOException e) {
20 throw new ClassNotFoundException();
21 }
22 }
23
24 private byte[] loadClassData(String className) throws IOException {
25 String fileName = classPath + File.separatorChar +
26 className.replace('.', File.separatorChar) + ".class";
27
28 try (InputStream is = new FileInputStream(fileName);
29 ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
30
31 byte[] buffer = new byte[1024];
32 int length;
33 while ((length = is.read(buffer)) != -1) {
34 baos.write(buffer, 0, length);
35 }
36 return baos.toByteArray();
37 }
38 }
39}
40
41// 使用
42MyClassLoader loader = new MyClassLoader("/custom/path");
43Class<?> clazz = loader.loadClass("com.example.MyClass");
44Object obj = clazz.newInstance();

6. 破坏双亲委派

场景1:JDBC驱动加载

java
1// 使用线程上下文类加载器
2Thread.currentThread().setContextClassLoader(customLoader);

场景2:OSGi模块化

  • 每个模块独立类加载器
  • 平级委派,不遵循双亲委派

场景3:热部署

java
1// 重新加载类
2public void reload(String className) {
3 // 创建新的类加载器
4 MyClassLoader newLoader = new MyClassLoader(classPath);
5 Class<?> clazz = newLoader.loadClass(className);
6 // 使用新类
7}

7. 类加载过程

1加载 -> 验证 -> 准备 -> 解析 -> 初始化

加载:

  • 读取.class文件
  • 生成Class对象

验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证

准备:

  • 分配内存
  • 设置默认值

解析:

  • 符号引用转为直接引用

初始化:

  • 执行静态代码块
  • 初始化静态变量

11. 什么是 Java 中的 JIT(Just-In-Time)?

答案:

JIT即时编译器是JVM的核心组件,在运行时将热点字节码编译成本地机器码。

1. JIT编译器类型

C1编译器(Client Compiler):

  • 快速编译
  • 简单优化
  • 适合客户端应用

C2编译器(Server Compiler):

  • 深度优化
  • 编译时间长
  • 适合服务端应用

2. 工作原理

java
1// 执行流程
2public void hotMethod() {
3 // 初始:解释执行
4 // 调用次数++
5
6 // 达到阈值(默认10000次)
7 if (invocationCount >= CompileThreshold) {
8 // 触发JIT编译
9 compileToNativeCode();
10 }
11
12 // 编译后:直接执行机器码
13}

3. 热点检测

方法调用计数器:

java
1// 统计方法调用次数
2int invocationCount = 0;
3
4public void method() {
5 invocationCount++;
6 if (invocationCount > threshold) {
7 // 触发编译
8 }
9}

回边计数器:

java
1// 统计循环次数
2for (int i = 0; i < 100000; i++) {
3 // 循环体
4 // 回边计数++
5}
6// 循环热点也会触发编译

4. 分层编译

1Level 0: 解释执行
2
3Level 1: C1编译(无profiling)
4
5Level 2: C1编译(有profiling)
6
7Level 3: C1编译(完全优化)
8
9Level 4: C2编译(激进优化)

5. JIT优化技术

方法内联:

java
1// 优化前
2public int add(int a, int b) {
3 return a + b;
4}
5
6public void test() {
7 int result = add(1, 2);
8}
9
10// 优化后(内联)
11public void test() {
12 int result = 1 + 2; // 直接内联
13}

逃逸分析:

java
1// 对象未逃逸,可在栈上分配
2public void method() {
3 Point p = new Point(1, 2);
4 int x = p.getX(); // 对象不会逃逸出方法
5}

循环展开:

java
1// 优化前
2for (int i = 0; i < 4; i++) {
3 sum += arr[i];
4}
5
6// 优化后
7sum += arr[0];
8sum += arr[1];
9sum += arr[2];
10sum += arr[3];

6. JVM参数

bash
1# 设置编译阈值
2-XX:CompileThreshold=10000
3
4# 禁用JIT
5-Xint
6
7# 只用编译模式
8-Xcomp
9
10# 打印编译信息
11-XX:+PrintCompilation
12
13# 打印内联信息
14-XX:+PrintInlining

7. 性能提升

java
1// 测试JIT效果
2public class JITTest {
3 public static void main(String[] args) {
4 long start = System.currentTimeMillis();
5
6 for (int i = 0; i < 100000; i++) {
7 compute(); // 前10000次慢,后续快
8 }
9
10 long end = System.currentTimeMillis();
11 System.out.println("耗时: " + (end - start) + "ms");
12 }
13
14 static int compute() {
15 int sum = 0;
16 for (int i = 0; i < 1000; i++) {
17 sum += i;
18 }
19 return sum;
20 }
21}

12. JIT 编译后的代码存在哪?

答案:

JIT编译后的本地代码存储在CodeCache(代码缓存区)中。

1. CodeCache位置

  • 属于非堆内存
  • 独立于Java堆
  • 使用本地内存

2. CodeCache结构

1CodeCache
2├── Non-nmethods (非方法代码)
3│ └── JVM内部代码
4├── Profiled nmethods (带profiling的代码)
5│ └── C1编译的代码
6└── Non-profiled nmethods (不带profiling的代码)
7 └── C2编译的代码

3. CodeCache大小配置

bash
1# 设置CodeCache大小
2-XX:ReservedCodeCacheSize=256m
3
4# 初始大小
5-XX:InitialCodeCacheSize=128m
6
7# 打印CodeCache使用情况
8-XX:+PrintCodeCache

4. 查看CodeCache使用

java
1// 运行时查看
2import sun.management.ManagementFactoryHelper;
3
4MemoryPoolMXBean codeCache = ManagementFactoryHelper
5 .getMemoryPools()
6 .stream()
7 .filter(pool -> pool.getName().contains("Code Cache"))
8 .findFirst()
9 .orElse(null);
10
11if (codeCache != null) {
12 MemoryUsage usage = codeCache.getUsage();
13 System.out.println("CodeCache已用: " + usage.getUsed() / 1024 / 1024 + "MB");
14 System.out.println("CodeCache最大: " + usage.getMax() / 1024 / 1024 + "MB");
15}

5. CodeCache满的影响

java
1// CodeCache满时的警告
2Java HotSpot(TM) 64-Bit Server VM warning:
3CodeCache is full. Compiler has been disabled.
4
5// 后果:
6// 1. JIT编译停止
7// 2. 只能解释执行
8// 3. 性能严重下降

6. 代码淘汰机制

java
1// 不常用的编译代码会被淘汰
2// 释放CodeCache空间
3// 需要时重新编译

7. 监控CodeCache

bash
1# 使用jconsole查看
2# Memory -> Code Cache
3
4# 使用jstat
5jstat -compiler <pid>
6
7# 输出编译统计
8Compiled Failed Invalid Time FailedType FailedMethod
91234 0 0 12.34 0

13. 什么是 Java 的 AOT(Ahead-Of-Time)?

答案:

AOT是提前编译技术,在程序运行前将字节码编译成本地机器码。

1. AOT vs JIT

特性AOTJIT
编译时机运行前运行时
启动速度
峰值性能中等
内存占用
优化程度静态优化动态优化

2. Java AOT实现

JDK 9引入:

bash
1# 使用jaotc编译
2jaotc --output libHelloWorld.so HelloWorld.class
3
4# 运行时加载
5java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

GraalVM Native Image:

bash
1# 编译成原生可执行文件
2native-image -jar application.jar
3
4# 生成可执行文件
5./application

3. AOT优势

快速启动:

java
1// JIT启动时间:2-3秒
2// AOT启动时间:0.1秒

低内存占用:

  • 无需JIT编译器
  • 无需CodeCache
  • 适合容器环境

4. AOT劣势

性能上限:

  • 缺少运行时信息
  • 无法做激进优化
  • 峰值性能低于JIT

文件体积:

  • 包含所有依赖
  • 可执行文件较大

5. 使用场景

适合AOT:

  • 微服务
  • Serverless
  • 容器应用
  • 命令行工具

适合JIT:

  • 长期运行的服务
  • 需要峰值性能
  • 复杂业务逻辑

6. GraalVM示例

bash
1# 安装GraalVM
2sdk install java 21.0.0.r11-grl
3
4# 编译Spring Boot应用
5./mvnw package -Pnative
6
7# 生成原生镜像
8native-image -jar target/app.jar
9
10# 运行
11./app
12# 启动时间:小于100ms
13# 内存占用:小于50MB

7. 配置参数

bash
1# 启用AOT
2-XX:+UseAOT
3
4# 指定AOT库
5-XX:AOTLibrary=./lib.so
6
7# 打印AOT信息
8-XX:+PrintAOT

14. 你了解 Java 的逃逸分析吗?

答案:

逃逸分析是JVM的一种优化技术,分析对象的作用域,进行栈上分配、标量替换等优化。

1. 什么是逃逸

对象逃逸:

  • 对象在方法外被引用
  • 对象被外部访问

未逃逸:

  • 对象只在方法内使用
  • 不会被外部访问

2. 逃逸类型

方法逃逸:

java
1// 对象逃逸出方法
2public User createUser() {
3 User user = new User();
4 return user; // 逃逸
5}

线程逃逸:

java
1// 对象被其他线程访问
2private User user;
3
4public void method() {
5 this.user = new User(); // 线程逃逸
6}

未逃逸:

java
1// 对象不逃逸
2public void method() {
3 User user = new User();
4 user.setName("Tom");
5 System.out.println(user.getName());
6 // user不会逃逸出方法
7}

3. 逃逸分析优化

栈上分配:

java
1// 优化前:对象在堆上分配
2public void method() {
3 Point p = new Point(1, 2); // 堆分配
4 int x = p.getX();
5}
6
7// 优化后:对象在栈上分配
8public void method() {
9 // 栈上分配,方法结束自动回收
10 // 无需GC
11}

标量替换:

java
1// 优化前
2public void method() {
3 Point p = new Point(1, 2);
4 int sum = p.x + p.y;
5}
6
7// 优化后:对象被拆解为标量
8public void method() {
9 int x = 1;
10 int y = 2;
11 int sum = x + y; // 直接使用标量
12}

锁消除:

java
1// 优化前
2public void method() {
3 StringBuffer sb = new StringBuffer();
4 sb.append("a"); // 内部有synchronized
5 sb.append("b");
6}
7
8// 优化后:sb未逃逸,消除锁
9public void method() {
10 StringBuffer sb = new StringBuffer();
11 sb.append("a"); // 去除synchronized
12 sb.append("b");
13}

4. 性能对比

java
1public class EscapeAnalysisTest {
2
3 // 未逃逸:栈上分配
4 public void noEscape() {
5 Point p = new Point(1, 2);
6 int sum = p.x + p.y;
7 }
8
9 // 逃逸:堆上分配
10 public Point escape() {
11 Point p = new Point(1, 2);
12 return p;
13 }
14
15 public static void main(String[] args) {
16 long start = System.currentTimeMillis();
17
18 for (int i = 0; i < 10000000; i++) {
19 noEscape(); // 快,无GC压力
20 }
21
22 long end = System.currentTimeMillis();
23 System.out.println("耗时: " + (end - start) + "ms");
24 }
25}

5. JVM参数

bash
1# 启用逃逸分析(默认开启)
2-XX:+DoEscapeAnalysis
3
4# 启用标量替换
5-XX:+EliminateAllocations
6
7# 启用锁消除
8-XX:+EliminateLocks
9
10# 打印逃逸分析结果
11-XX:+PrintEscapeAnalysis

6. 实际应用

java
1// 优化建议:尽量让对象不逃逸
2public class Service {
3
4 // 好:对象不逃逸
5 public void process() {
6 Data data = new Data();
7 data.calculate();
8 // data不逃逸,可栈上分配
9 }
10
11 // 差:对象逃逸
12 private Data data;
13 public void process2() {
14 this.data = new Data(); // 逃逸
15 }
16}

15. Java 中的强引用、软引用、弱引用和虚引用分别是什么?

答案:

Java提供四种引用类型,用于不同的内存管理场景。

1. 强引用(Strong Reference)

定义:

  • 最常见的引用
  • 只要强引用存在,对象不会被回收

示例:

java
1// 强引用
2Object obj = new Object();
3
4// 只要obj存在,对象不会被GC
5// 即使内存不足也不会回收
6// 除非obj = null

特点:

  • 宁可OOM也不回收
  • 最常用的引用类型

2. 软引用(Soft Reference)

定义:

  • 内存不足时才回收
  • 适合缓存场景

示例:

java
1// 创建软引用
2SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);
3
4// 获取对象
5byte[] data = softRef.get();
6if (data != null) {
7 // 对象还在
8} else {
9 // 对象已被回收
10}

应用场景:

java
1// 图片缓存
2public class ImageCache {
3 private Map<String, SoftReference<Image>> cache = new HashMap<>();
4
5 public Image getImage(String path) {
6 SoftReference<Image> ref = cache.get(path);
7
8 if (ref != null) {
9 Image img = ref.get();
10 if (img != null) {
11 return img; // 缓存命中
12 }
13 }
14
15 // 缓存未命中,加载图片
16 Image img = loadImage(path);
17 cache.put(path, new SoftReference<>(img));
18 return img;
19 }
20}

3. 弱引用(Weak Reference)

定义:

  • GC时必定回收
  • 生命周期更短

示例:

java
1// 创建弱引用
2WeakReference<Object> weakRef = new WeakReference<>(new Object());
3
4// 获取对象
5Object obj = weakRef.get();
6if (obj != null) {
7 // 对象还在
8}
9
10// GC后
11System.gc();
12obj = weakRef.get(); // null

应用场景:

java
1// ThreadLocal实现
2public class ThreadLocal<T> {
3 static class Entry extends WeakReference<ThreadLocal<?>> {
4 Object value;
5
6 Entry(ThreadLocal<?> k, Object v) {
7 super(k); // key是弱引用
8 value = v;
9 }
10 }
11}
12
13// WeakHashMap
14WeakHashMap<Key, Value> map = new WeakHashMap<>();
15Key key = new Key();
16map.put(key, value);
17
18key = null; // 去除强引用
19System.gc(); // GC后,map中的entry被自动移除

4. 虚引用(Phantom Reference)

定义:

  • 最弱的引用
  • 无法通过get()获取对象
  • 用于跟踪对象回收

示例:

java
1// 创建虚引用(必须配合引用队列)
2ReferenceQueue<Object> queue = new ReferenceQueue<>();
3PhantomReference<Object> phantomRef = new PhantomReference<>(
4 new Object(), queue
5);
6
7// get()永远返回null
8Object obj = phantomRef.get(); // null
9
10// 对象被回收时,虚引用会进入队列
11Reference<?> ref = queue.poll();
12if (ref != null) {
13 // 对象已被回收,执行清理工作
14}

应用场景:

java
1// 监控对象回收
2public class ResourceMonitor {
3 private ReferenceQueue<Resource> queue = new ReferenceQueue<>();
4
5 public void monitor(Resource resource) {
6 PhantomReference<Resource> ref =
7 new PhantomReference<>(resource, queue);
8
9 // 启动线程监控
10 new Thread(() -> {
11 try {
12 Reference<?> r = queue.remove(); // 阻塞等待
13 System.out.println("资源已被回收");
14 // 执行清理工作
15 cleanup();
16 } catch (InterruptedException e) {
17 e.printStackTrace();
18 }
19 }).start();
20 }
21}

5. 引用对比

引用类型回收时机用途get()返回
强引用永不回收普通对象对象
软引用内存不足缓存对象或null
弱引用GC时缓存、防止内存泄漏对象或null
虚引用GC时跟踪回收永远null

6. 引用队列

java
1// 配合引用队列使用
2ReferenceQueue<Object> queue = new ReferenceQueue<>();
3
4SoftReference<Object> softRef = new SoftReference<>(new Object(), queue);
5WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);
6
7// 对象被回收后,引用会进入队列
8System.gc();
9
10Reference<?> ref = queue.poll();
11if (ref != null) {
12 // 执行清理工作
13 System.out.println("对象已被回收");
14}

7. 实际应用建议

java
1// 1. 普通对象:使用强引用
2Object obj = new Object();
3
4// 2. 缓存:使用软引用
5SoftReference<Cache> cache = new SoftReference<>(new Cache());
6
7// 3. 防止内存泄漏:使用弱引用
8WeakHashMap<Key, Value> map = new WeakHashMap<>();
9
10// 4. 监控回收:使用虚引用
11PhantomReference<Resource> ref = new PhantomReference<>(resource, queue);

16. Java 中常见的垃圾收集器有哪些?

答案:

Java提供多种垃圾收集器,适用于不同场景。

1. Serial收集器

特点:

  • 单线程收集
  • 简单高效
  • 适合单核CPU

使用场景:

  • 客户端应用
  • 小内存环境

参数:

bash
1# 新生代使用Serial
2-XX:+UseSerialGC

2. ParNew收集器

特点:

  • Serial的多线程版本
  • 新生代收集器
  • 配合CMS使用

参数:

bash
1-XX:+UseParNewGC

3. Parallel Scavenge收集器

特点:

  • 关注吞吐量
  • 自适应调节
  • 适合后台计算

参数:

bash
1-XX:+UseParallelGC
2# 设置吞吐量目标
3-XX:GCTimeRatio=99
4# 最大暂停时间
5-XX:MaxGCPauseMillis=100

4. CMS收集器(Concurrent Mark Sweep)

特点:

  • 并发收集
  • 低停顿
  • 适合响应时间敏感应用

阶段:

11. 初始标记(STW)
22. 并发标记
33. 重新标记(STW)
44. 并发清除

参数:

bash
1-XX:+UseConcMarkSweepGC
2# 触发GC的堆使用率
3-XX:CMSInitiatingOccupancyFraction=70

缺点:

  • 产生内存碎片
  • 并发模式失败
  • CPU资源敏感

5. G1收集器(Garbage First)

特点:

  • 面向服务端
  • 可预测停顿
  • 整堆收集
  • 无内存碎片

内存布局:

1Heap划分为多个Region
2每个Region可以是Eden、Survivor或Old

参数:

bash
1-XX:+UseG1GC
2# 期望停顿时间
3-XX:MaxGCPauseMillis=200
4# 堆大小
5-Xmx4g

6. ZGC收集器

特点:

  • 超低延迟(小于10ms)
  • 支持TB级堆
  • 并发收集

参数:

bash
1-XX:+UseZGC
2-Xmx16g

7. Shenandoah GC

特点:

  • 低停顿
  • 并发整理
  • 与堆大小无关的停顿时间

8. 收集器组合

新生代老年代说明
SerialSerial Old单线程
ParNewCMS低延迟
Parallel ScavengeParallel Old高吞吐
G1G1平衡

9. 选择建议

小应用(小于100MB):

  • Serial GC

中等应用(几GB):

  • G1 GC

大内存应用(>10GB):

  • ZGC或Shenandoah

低延迟要求:

  • CMS或G1

高吞吐量要求:

  • Parallel GC

17. Java 中如何判断对象是否是垃圾?不同实现方式有何区别?

答案:

Java主要使用可达性分析算法判断对象是否为垃圾。

1. 引用计数法(Java未采用)

原理:

  • 为对象添加引用计数器
  • 引用+1,失效-1
  • 计数为0时回收

优点:

  • 实现简单
  • 判定高效

缺点:

java
1// 无法解决循环引用
2public class CircularReference {
3 public Object ref;
4
5 public static void main(String[] args) {
6 CircularReference obj1 = new CircularReference();
7 CircularReference obj2 = new CircularReference();
8
9 obj1.ref = obj2;
10 obj2.ref = obj1;
11
12 obj1 = null;
13 obj2 = null;
14
15 // 两个对象互相引用,计数不为0
16 // 但实际已成为垃圾
17 }
18}

2. 可达性分析算法(Java采用)

原理:

  • 从GC Roots开始向下搜索
  • 不可达的对象即为垃圾

GC Roots包括:

java
1// 1. 虚拟机栈中的引用
2public void method() {
3 Object obj = new Object(); // 栈中引用
4}
5
6// 2. 方法区中的静态变量
7public class Test {
8 private static Object staticObj = new Object();
9}
10
11// 3. 方法区中的常量
12public class Test {
13 private static final Object CONSTANT = new Object();
14}
15
16// 4. 本地方法栈中的引用
17native void nativeMethod();
18
19// 5. 活跃线程
20Thread thread = new Thread();

可达性分析过程:

1GC Roots
2 |
3 ├─> Object A (可达)
4 │ |
5 │ └─> Object B (可达)
6 |
7 └─> Object C (可达)
8
9Object D (不可达,垃圾)
10 |
11 └─> Object E (不可达,垃圾)

3. 对象的生死判定

第一次标记:

java
1// 不可达对象被第一次标记
2// 判断是否需要执行finalize()
3if (!obj.isReachable() && obj.hasFinalize()) {
4 // 放入F-Queue队列
5 fQueue.add(obj);
6}

finalize()方法:

java
1public class FinalizeTest {
2 public static FinalizeTest instance;
3
4 @Override
5 protected void finalize() throws Throwable {
6 System.out.println("finalize被调用");
7 // 自救:重新建立引用
8 instance = this;
9 }
10
11 public static void main(String[] args) throws Exception {
12 instance = new FinalizeTest();
13
14 // 第一次GC
15 instance = null;
16 System.gc();
17 Thread.sleep(500);
18
19 if (instance != null) {
20 System.out.println("对象存活"); // 自救成功
21 }
22
23 // 第二次GC
24 instance = null;
25 System.gc();
26 Thread.sleep(500);
27
28 if (instance == null) {
29 System.out.println("对象死亡"); // finalize只执行一次
30 }
31 }
32}

4. 引用类型判定

java
1// 强引用:永不回收
2Object obj = new Object();
3
4// 软引用:内存不足时回收
5SoftReference<Object> soft = new SoftReference<>(new Object());
6
7// 弱引用:GC时回收
8WeakReference<Object> weak = new WeakReference<>(new Object());
9
10// 虚引用:无法获取对象
11PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);

5. 方法区回收

类卸载条件:

java
1// 1. 该类所有实例已被回收
2// 2. 加载该类的ClassLoader已被回收
3// 3. 该类的Class对象没有被引用
4
5// 示例:动态代理类可能被卸载
6Proxy.newProxyInstance(...);

18. 为什么 Java 的垃圾收集器将堆分为老年代和新生代?

答案:

分代收集基于两个假说,针对不同特征的对象采用不同的回收策略。

1. 分代假说

弱分代假说:

  • 大部分对象朝生夕灭
  • 98%的对象在创建后很快死亡

强分代假说:

  • 熬过多次GC的对象难以消亡
  • 存活时间长的对象会继续存活

2. 分代结构

1Java Heap
2├── Young Generation (新生代)
3│ ├── Eden (80%)
4│ ├── Survivor 0 (10%)
5│ └── Survivor 1 (10%)
6└── Old Generation (老年代)

3. 分代优势

针对性回收:

java
1// 新生代:频繁GC,快速回收
2// 大部分对象在这里死亡
3for (int i = 0; i < 1000000; i++) {
4 Object temp = new Object(); // 临时对象
5 // 方法结束后成为垃圾
6}
7
8// 老年代:少量GC,长期存活
9public class Service {
10 private static Cache cache = new Cache(); // 长期存活
11}

提高效率:

1新生代GC(Minor GC):
2- 频率高
3- 速度快(复制算法)
4- 停顿时间短
5
6老年代GC(Major GC):
7- 频率低
8- 速度慢(标记-整理)
9- 停顿时间长

4. 对象分配流程

java
1// 1. 新对象在Eden区分配
2Object obj = new Object();
3
4// 2. Eden区满,触发Minor GC
5// 存活对象复制到Survivor
6
7// 3. 多次GC后仍存活,晋升到老年代
8// 默认15次(-XX:MaxTenuringThreshold=15)
9
10// 4. 大对象直接进入老年代
11byte[] bigObj = new byte[10 * 1024 * 1024]; // 10MB

5. 性能对比

不分代的问题:

1每次GC都扫描整个堆
2→ 效率低下
3→ 停顿时间长

分代的优势:

1Minor GC只扫描新生代
2→ 扫描范围小
3→ 速度快
4→ 停顿时间短

6. 实际数据

1新生代对象存活率:1-2%
2老年代对象存活率:>90%
3
4Minor GC频率:秒级
5Major GC频率:分钟级
6
7Minor GC耗时:几毫秒
8Major GC耗时:几百毫秒

19. 为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?

答案:

元空间解决了永久代的诸多问题,提供更灵活的内存管理。

1. 永久代的问题

问题1:大小难以确定

bash
1# JDK 7永久代配置
2-XX:PermSize=64m
3-XX:MaxPermSize=256m
4
5# 问题:
6# - 设置太小:容易OOM
7# - 设置太大:浪费内存
8# - 难以预估合适大小

问题2:容易OOM

java
1// 动态生成大量类
2while (true) {
3 Enhancer enhancer = new Enhancer();
4 enhancer.setSuperclass(Object.class);
5 enhancer.create();
6}
7// java.lang.OutOfMemoryError: PermGen space

问题3:Full GC效率低

1永久代GC需要Full GC
2→ 停顿时间长
3→ 影响应用性能

问题4:内存碎片

1永久代使用堆内存
2→ 产生碎片
3→ 影响分配效率

2. 元空间的优势

优势1:使用本地内存

bash
1# JDK 8元空间配置
2-XX:MetaspaceSize=128m
3-XX:MaxMetaspaceSize=512m
4
5# 优势:
6# - 不占用堆内存
7# - 理论上只受物理内存限制
8# - 默认无上限(自动扩展)

优势2:自动扩展

java
1// 元空间会根据需要自动扩展
2// 无需精确设置大小
3// 减少OOM风险

优势3:GC效率提升

1元空间GC独立进行
2→ 不影响堆GC
3→ 减少Full GC频率

优势4:简化调优

bash
1# 永久代:需要精确调优
2-XX:PermSize=128m
3-XX:MaxPermSize=256m
4
5# 元空间:通常只设置最大值
6-XX:MaxMetaspaceSize=512m

3. 内存布局变化

JDK 7:

1Heap
2├── Young Generation
3├── Old Generation
4└── Permanent Generation
5 ├── 类元数据
6 ├── 字符串常量池
7 └── 静态变量

JDK 8:

1Heap
2├── Young Generation
3├── Old Generation
4└── 字符串常量池(移到堆)
5
6Metaspace(本地内存)
7└── 类元数据

4. 迁移影响

字符串常量池迁移:

java
1// JDK 7+:字符串常量池在堆中
2String s1 = "hello";
3String s2 = new String("hello");
4String s3 = s2.intern();
5
6// s1和s3指向堆中的常量池
7System.out.println(s1 == s3); // true

静态变量迁移:

java
1// JDK 8:静态变量随Class对象存储在堆中
2public class Test {
3 private static Object obj = new Object();
4 // obj存储在堆中
5}

5. 实际案例

永久代OOM:

java
1// JDK 7
2// 大量使用CGLib动态代理
3for (int i = 0; i < 100000; i++) {
4 Enhancer enhancer = new Enhancer();
5 enhancer.setSuperclass(MyClass.class);
6 enhancer.create();
7}
8// OutOfMemoryError: PermGen space

元空间自动扩展:

java
1// JDK 8
2// 相同代码不会轻易OOM
3// 元空间自动扩展
4// 除非达到MaxMetaspaceSize或物理内存限制

6. 监控对比

bash
1# JDK 7监控永久代
2jstat -gcpermcapacity <pid>
3
4# JDK 8监控元空间
5jstat -gcmetacapacity <pid>

20. 为什么 Java 新生代被划分为 S0、S1 和 Eden 区?

答案:

新生代的三区划分是为了高效实现复制算法,避免内存碎片。

1. 新生代结构

1Young Generation
2├── Eden (80%)
3├── Survivor 0 (10%)
4└── Survivor 1 (10%)
5
6默认比例:8:1:1

2. 为什么需要两个Survivor

问题:只有一个Survivor

1Eden → Survivor → Old
2
3问题:
41. 内存碎片
52. 无法区分不同年龄的对象
63. 晋升策略难以实现

解决:使用两个Survivor

1Eden + S0 → S1
2Eden + S1 → S0
3
4优势:
51. 无内存碎片(复制算法)
62. 对象年龄递增
73. 灵活的晋升策略

3. Minor GC流程

第一次GC:

java
1// 初始状态
2Eden: [对象A, B, C, D, E]
3S0: []
4S1: []
5
6// GC后(假设A、B存活)
7Eden: []
8S0: [A(age=1), B(age=1)]
9S1: []

第二次GC:

java
1// GC前
2Eden: [对象F, G, H]
3S0: [A(age=1), B(age=1)]
4S1: []
5
6// GC后(假设A、F存活)
7Eden: []
8S0: []
9S1: [A(age=2), F(age=1)]

多次GC后:

java
1// 对象年龄达到阈值(默认15)
2if (age >= MaxTenuringThreshold) {
3 // 晋升到老年代
4 moveToOld(obj);
5}

4. 为什么是8:1:1比例

依据:

1新生代对象存活率:1-2%
2Eden区:80%
3Survivor:10% × 2 = 20%
4
5每次GC:
6- Eden + 1个Survivor(90%)
7- 存活对象复制到另一个Survivor
8- 10%空间足够容纳存活对象

内存利用率:

1可用空间:Eden + 1个Survivor = 90%
2浪费空间:1个Survivor = 10%
3
4对比传统复制算法(50%利用率)
5→ 大幅提升内存利用率

5. 空间分配担保

java
1// 如果Survivor空间不足
2if (survivorSize < aliveObjects) {
3 // 直接晋升到老年代
4 moveToOld(objects);
5}
6
7// 老年代空间检查
8if (oldGenFreeSpace < youngGenUsedSpace) {
9 // 触发Full GC
10 fullGC();
11}

6. 实际示例

java
1public class GCTest {
2 public static void main(String[] args) {
3 // 1. 对象在Eden分配
4 byte[] obj1 = new byte[1024 * 1024]; // 1MB
5
6 // 2. Eden满,触发Minor GC
7 // obj1存活,复制到S0
8
9 // 3. 继续分配
10 byte[] obj2 = new byte[1024 * 1024];
11
12 // 4. 再次GC
13 // obj1复制到S1,age=2
14 // obj2复制到S1,age=1
15
16 // 5. 15次GC后
17 // obj1晋升到老年代
18 }
19}

7. 参数配置

bash
1# 设置新生代大小
2-Xmn512m
3
4# 设置Eden和Survivor比例
5-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
6
7# 设置晋升年龄阈值
8-XX:MaxTenuringThreshold=15
9
10# 打印GC详情
11-XX:+PrintGCDetails

8. 优化建议

java
1// 1. 避免创建大对象
2byte[] big = new byte[10 * 1024 * 1024]; // 直接进老年代
3
4// 2. 复用对象
5StringBuilder sb = new StringBuilder(); // 复用
6
7// 3. 及时释放引用
8list.clear(); // 帮助GC
9list = null;

21. 什么是三色标记算法?

答案:

三色标记算法是并发GC中用于标记对象的核心算法,解决并发标记时的对象漏标问题。

1. 三色定义

白色(White):

  • 未被访问的对象
  • 标记结束后仍为白色的对象会被回收

灰色(Gray):

  • 已被访问但其引用的对象未全部访问
  • 处于待处理状态

黑色(Black):

  • 已被访问且其引用的对象也已访问
  • 不会被回收

2. 标记过程

java
1// 初始状态:所有对象为白色
2初始:所有对象 = 白色
3
4// 从GC Roots开始标记
5GC Roots -> 灰色
6
7// 标记过程
8while (存在灰色对象) {
9 取出一个灰色对象
10 标记为黑色
11 将其引用的白色对象标记为灰色
12}
13
14// 结束:白色对象即为垃圾

3. 标记示例

1初始状态:
2GC Root -> A -> B -> C
3所有对象:白色
4
5第1步:标记GC Root引用的A
6A: 灰色
7B, C: 白色
8
9第2步:处理A,标记B
10A: 黑色
11B: 灰色
12C: 白色
13
14第3步:处理B,标记C
15A, B: 黑色
16C: 灰色
17
18第4步:处理C
19A, B, C: 黑色
20
21结束:白色对象被回收

4. 并发标记问题

对象漏标问题:

java
1// 并发标记时,应用线程修改引用
2初始状态:
3A() -> B() -> C()
4
5// 应用线程执行:
6A.ref = C; // A引用C
7B.ref = null; // B不再引用C
8
9结果:
10A() -> C() // C被漏标!
11B()
12
13// C应该存活但被标记为白色
14// 会被错误回收

漏标条件(同时满足):

  1. 黑色对象指向白色对象
  2. 灰色对象到白色对象的引用被删除

5. 解决方案

增量更新(Incremental Update):

java
1// CMS使用
2// 当黑色对象引用白色对象时
3if (black.ref = white) {
4 // 将黑色对象重新标记为灰色
5 black -> 灰色
6}
7
8// 破坏条件1

原始快照(SATB - Snapshot At The Beginning):

java
1// G1使用
2// 当删除引用时
3if (gray.ref = null) {
4 // 记录被删除的引用
5 记录(gray -> white)
6}
7
8// 破坏条件2

6. 写屏障实现

增量更新写屏障:

java
1// 伪代码
2void writeBarrier(Object obj, Object ref) {
3 if (obj.isBlack() && ref.isWhite()) {
4 // 重新标记为灰色
5 obj.setGray();
6 }
7 obj.ref = ref;
8}

SATB写屏障:

java
1// 伪代码
2void writeBarrier(Object obj, Object oldRef) {
3 if (oldRef != null && oldRef.isWhite()) {
4 // 记录旧引用
5 satbQueue.add(oldRef);
6 }
7 obj.ref = newRef;
8}

7. 实际应用

CMS(增量更新):

1初始标记(STW)
2
3并发标记(增量更新)
4
5重新标记(STW,处理变化)
6
7并发清除

G1(SATB):

1初始标记(STW)
2
3并发标记(SATB)
4
5最终标记(STW,处理SATB队列)
6
7筛选回收

8. 性能对比

特性增量更新SATB
使用者CMSG1
重新标记工作量较大较小
浮动垃圾较少较多
实现复杂度简单复杂

22. Java 中的 young GC、old GC、full GC 和 mixed GC 的区别是什么?

答案:

不同类型的GC针对不同的内存区域,有不同的触发条件和回收策略。

1. Young GC(Minor GC)

定义:

  • 只回收新生代
  • 最频繁的GC类型

触发条件:

java
1// Eden区满时触发
2if (eden.isFull()) {
3 youngGC();
4}

回收过程:

11. Eden区存活对象 -> Survivor
22. Survivor区存活对象 -> 另一个Survivor
33. 年龄达标对象 -> 老年代
44. 清空Eden和一个Survivor

特点:

  • 频率高(秒级)
  • 速度快(几毫秒)
  • 使用复制算法
  • STW时间短

2. Old GC(Major GC)

定义:

  • 只回收老年代
  • 较少使用的术语

触发条件:

java
1// 老年代空间不足
2if (oldGen.freeSpace < threshold) {
3 oldGC();
4}

特点:

  • 频率低
  • 速度慢
  • 使用标记-整理算法

注意:

  • Major GC通常伴随Full GC
  • 很多情况下两者等价

3. Full GC

定义:

  • 回收整个堆(新生代+老年代)
  • 回收方法区

触发条件:

java
1// 1. 老年代空间不足
2if (oldGen.freeSpace < requiredSpace) {
3 fullGC();
4}
5
6// 2. 方法区空间不足
7if (metaspace.isFull()) {
8 fullGC();
9}
10
11// 3. 空间分配担保失败
12if (promotionFailed) {
13 fullGC();
14}
15
16// 4. 显式调用System.gc()
17System.gc(); // 建议JVM执行Full GC
18
19// 5. CMS并发失败
20if (concurrentModeFailure) {
21 fullGC();
22}

特点:

  • 频率最低
  • 耗时最长(几百毫秒到几秒)
  • STW时间长
  • 影响应用性能

4. Mixed GC

定义:

  • G1特有的GC类型
  • 回收新生代+部分老年代

触发条件:

java
1// 老年代占用达到阈值
2if (oldGenOccupancy > InitiatingHeapOccupancyPercent) {
3 mixedGC();
4}
5
6// 默认45%
7-XX:InitiatingHeapOccupancyPercent=45

回收过程:

11. 回收所有新生代Region
22. 回收部分老年代Region(价值最高的)
33. 根据停顿时间目标选择Region数量

特点:

  • G1独有
  • 可预测停顿时间
  • 增量回收老年代

5. GC类型对比

GC类型回收区域频率耗时STW
Young GC新生代
Old GC老年代
Full GC整个堆最低最长最长
Mixed GC新生代+部分老年代可控

6. 实际案例

Young GC示例:

java
1public class YoungGCTest {
2 public static void main(String[] args) {
3 for (int i = 0; i < 1000; i++) {
4 byte[] temp = new byte[1024 * 1024]; // 1MB
5 // 频繁触发Young GC
6 }
7 }
8}
9// 日志:[GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] ...

Full GC示例:

java
1public class FullGCTest {
2 public static void main(String[] args) {
3 List<byte[]> list = new ArrayList<>();
4 while (true) {
5 list.add(new byte[1024 * 1024]); // 1MB
6 // 最终触发Full GC
7 }
8 }
9}
10// 日志:[Full GC (Ergonomics) [PSYoungGen: 2048K->0K] [ParOldGen: 8192K->8000K] ...

7. 监控GC

bash
1# 打印GC日志
2-XX:+PrintGCDetails
3-XX:+PrintGCDateStamps
4
5# 使用jstat监控
6jstat -gc <pid> 1000
7
8# 输出示例
9S0C S1C S0U S1U EC EU OC OU YGC YGCT FGC FGCT
102048.0 2048.0 0.0 512.0 16384.0 8192.0 40960.0 20480.0 100 0.500 5 2.000

23. 什么条件会触发 Java 的 young GC?

答案:

Young GC主要在Eden区空间不足时触发。

1. 主要触发条件

Eden区满:

java
1// 最常见的触发条件
2if (eden.freeSpace < objectSize) {
3 // 触发Young GC
4 youngGC();
5}

2. 对象分配流程

java
1public Object allocate(int size) {
2 // 1. 尝试在TLAB分配
3 if (tlab.canAllocate(size)) {
4 return tlab.allocate(size);
5 }
6
7 // 2. TLAB不足,尝试在Eden分配
8 if (eden.canAllocate(size)) {
9 return eden.allocate(size);
10 }
11
12 // 3. Eden不足,触发Young GC
13 youngGC();
14
15 // 4. GC后重试分配
16 if (eden.canAllocate(size)) {
17 return eden.allocate(size);
18 }
19
20 // 5. 仍不足,直接分配到老年代
21 return oldGen.allocate(size);
22}

3. 触发时机示例

java
1public class YoungGCTrigger {
2 public static void main(String[] args) {
3 // 假设Eden区大小为10MB
4
5 // 分配9MB,未触发GC
6 byte[] obj1 = new byte[9 * 1024 * 1024];
7
8 // 分配2MB,Eden不足,触发Young GC
9 byte[] obj2 = new byte[2 * 1024 * 1024];
10
11 // GC日志:
12 // [GC (Allocation Failure) [PSYoungGen: 9216K->1024K(10240K)] ...
13 }
14}

4. 分配失败场景

场景1:Eden区满

java
1// Eden: 8MB已用,2MB空闲
2// 分配3MB对象
3byte[] obj = new byte[3 * 1024 * 1024];
4
5// 流程:
6// 1. Eden空间不足
7// 2. 触发Young GC
8// 3. 清理Eden
9// 4. 重新分配

场景2:大对象直接进老年代

java
1// 对象大于Eden区
2byte[] bigObj = new byte[20 * 1024 * 1024]; // 20MB
3
4// 流程:
5// 1. 对象大于Eden
6// 2. 不触发Young GC
7// 3. 直接分配到老年代

5. 空间分配担保

java
1// Young GC前检查
2if (oldGen.freeSpace < youngGen.usedSpace) {
3 // 老年代空间可能不足
4 if (HandlePromotionFailure) {
5 // 允许冒险,执行Young GC
6 youngGC();
7 } else {
8 // 不允许冒险,执行Full GC
9 fullGC();
10 }
11}

6. 相关JVM参数

bash
1# 设置新生代大小
2-Xmn512m
3
4# 设置Eden和Survivor比例
5-XX:SurvivorRatio=8
6
7# 打印GC详情
8-XX:+PrintGCDetails
9
10# 打印GC原因
11-XX:+PrintGCCause

7. 监控Young GC

bash
1# 使用jstat
2jstat -gcutil <pid> 1000
3
4# 输出
5 S0 S1 E O M YGC YGCT
6 0.00 50.00 75.00 30.00 95.00 100 0.500
7
8# E: Eden使用率75%
9# YGC: Young GC次数100次
10# YGCT: Young GC总耗时0.5秒

8. 优化建议

java
1// 1. 合理设置新生代大小
2-Xmn1g // 根据应用特点调整
3
4// 2. 避免频繁创建大对象
5// 差:
6for (int i = 0; i < 1000; i++) {
7 byte[] temp = new byte[10 * 1024 * 1024]; // 频繁触发GC
8}
9
10// 好:
11byte[] buffer = new byte[10 * 1024 * 1024]; // 复用
12for (int i = 0; i < 1000; i++) {
13 // 使用buffer
14}
15
16// 3. 对象池复用
17ObjectPool<Buffer> pool = new ObjectPool<>();
18Buffer buffer = pool.borrow();
19// 使用
20pool.return(buffer);

24. 什么情况下会触发 Java 的 Full GC?

答案:

Full GC在多种情况下触发,通常意味着严重的性能问题。

1. 老年代空间不足

java
1// 场景1:大对象直接进入老年代
2byte[] bigObj = new byte[100 * 1024 * 1024]; // 100MB
3
4// 场景2:Young GC后晋升对象过多
5if (oldGen.freeSpace < promotionSize) {
6 fullGC();
7}
8
9// 场景3:长期存活对象累积
10public class Service {
11 private static List<Object> cache = new ArrayList<>();
12
13 public void addCache() {
14 cache.add(new Object()); // 持续累积
15 // 最终老年代满,触发Full GC
16 }
17}

2. 方法区(元空间)满

java
1// 动态生成大量类
2while (true) {
3 Enhancer enhancer = new Enhancer();
4 enhancer.setSuperclass(Object.class);
5 enhancer.setUseCache(false);
6 enhancer.create(); // 生成类
7}
8
9// 元空间满,触发Full GC
10// java.lang.OutOfMemoryError: Metaspace

3. 空间分配担保失败

java
1// Young GC前检查
2long avgPromotionSize = getAveragePromotionSize();
3
4if (oldGen.freeSpace < avgPromotionSize) {
5 // 担保失败,触发Full GC
6 fullGC();
7} else {
8 // 执行Young GC
9 youngGC();
10}
11
12// 如果Young GC后仍无法晋升
13if (promotionFailed) {
14 // 再次触发Full GC
15 fullGC();
16}

4. CMS并发失败

java
1// CMS并发标记期间
2// 老年代空间不足以容纳新晋升对象
3if (oldGen.freeSpace < promotionSize) {
4 // Concurrent Mode Failure
5 // 降级为Serial Old,执行Full GC
6 fullGC();
7}
8
9// 日志:
10// [CMS-concurrent-mark: 0.100/0.100 secs]
11// [CMS Concurrent Mode Failure]
12// [Full GC (Allocation Failure) ...

5. 显式调用System.gc()

java
1// 不推荐
2System.gc(); // 建议JVM执行Full GC
3
4// 可以禁用
5-XX:+DisableExplicitGC
6
7// 或改为并发GC
8-XX:+ExplicitGCInvokesConcurrent

6. Dump堆内存

bash
1# jmap触发Full GC
2jmap -dump:live,format=b,file=heap.bin <pid>
3
4# live参数会先执行Full GC

7. 晋升失败

java
1// Promotion Failed
2// Survivor空间不足,对象直接晋升老年代
3// 但老年代也空间不足
4
5// 示例
6public class PromotionFailure {
7 public static void main(String[] args) {
8 List<byte[]> list = new ArrayList<>();
9
10 for (int i = 0; i < 1000; i++) {
11 // 创建对象
12 byte[] obj = new byte[1024 * 1024];
13 list.add(obj);
14
15 // 触发Young GC
16 // 对象晋升失败
17 // 触发Full GC
18 }
19 }
20}

8. Full GC触发条件汇总

触发条件说明频率影响
老年代满最常见
元空间满类加载过多
担保失败空间预估不足
CMS失败并发收集失败极大
System.gc()显式调用取决于代码
晋升失败空间碎片

9. 监控Full GC

bash
1# GC日志
2-XX:+PrintGCDetails
3-XX:+PrintGCDateStamps
4-Xloggc:gc.log
5
6# 日志示例
7[Full GC (Allocation Failure)
8 [PSYoungGen: 2048K->0K(2560K)]
9 [ParOldGen: 8192K->7000K(10240K)]
10 10240K->7000K(12800K),
11 [Metaspace: 3000K->3000K(1056768K)],
12 0.5000000 secs]
13
14# jstat监控
15jstat -gcutil <pid> 1000
16 S0 S1 E O M FGC FGCT
17 0.00 0.00 10.00 95.00 90.00 10 5.000

10. 避免Full GC

java
1// 1. 合理设置堆大小
2-Xms4g -Xmx4g // 初始和最大堆一致
3
4// 2. 调整新生代和老年代比例
5-XX:NewRatio=2 // 老年代:新生代 = 2:1
6
7// 3. 避免大对象
8// 差:
9byte[] big = new byte[10 * 1024 * 1024];
10
11// 好:分批处理
12for (int i = 0; i < 10; i++) {
13 byte[] small = new byte[1024 * 1024];
14 process(small);
15}
16
17// 4. 及时释放引用
18list.clear();
19cache.evict();
20
21// 5. 使用对象池
22ObjectPool<Buffer> pool = new ObjectPool<>();
23
24// 6. 选择合适的GC
25-XX:+UseG1GC // G1避免Full GC
26-XX:MaxGCPauseMillis=200

25. 什么是 Java 的 PLAB?

答案:

PLAB(Promotion Local Allocation Buffer)是JVM为每个线程在老年代分配的私有缓冲区,用于优化对象晋升性能。

1. PLAB定义

作用:

  • 线程私有的老年代分配缓冲区
  • 避免多线程竞争
  • 提高对象晋升效率

类比TLAB:

1TLAB: 新生代的线程本地分配缓冲区
2PLAB: 老年代的线程本地分配缓冲区

2. 为什么需要PLAB

问题:多线程晋升竞争

java
1// Young GC时,多个线程同时晋升对象
2Thread 1: 晋升对象A到老年代
3Thread 2: 晋升对象B到老年代
4Thread 3: 晋升对象C到老年代
5
6// 没有PLAB:需要同步
7synchronized (oldGen) {
8 oldGen.allocate(obj); // 性能瓶颈
9}

解决:使用PLAB

java
1// 每个线程有自己的PLAB
2Thread 1: PLAB1 -> 老年代
3Thread 2: PLAB2 -> 老年代
4Thread 3: PLAB3 -> 老年代
5
6// 无需同步,并行晋升

3. PLAB工作原理

java
1// 对象晋升流程
2public void promoteObject(Object obj) {
3 // 1. 尝试在PLAB中分配
4 if (plab.canAllocate(obj.size())) {
5 plab.allocate(obj);
6 return;
7 }
8
9 // 2. PLAB空间不足,申请新PLAB
10 if (obj.size() < PLAB_SIZE) {
11 plab = allocateNewPLAB();
12 plab.allocate(obj);
13 return;
14 }
15
16 // 3. 对象太大,直接在老年代分配(需要同步)
17 synchronized (oldGen) {
18 oldGen.allocate(obj);
19 }
20}

4. PLAB大小

默认大小:

bash
1# 动态调整
2# 根据晋升对象大小自动调整PLAB大小
3
4# 查看PLAB统计
5-XX:+PrintPLAB
6
7# 输出示例
8PLAB: 1024K, waste: 10K, refills: 5

影响因素:

11. 晋升对象的平均大小
22. 晋升频率
33. 线程数量
44. 老年代可用空间

5. PLAB vs TLAB对比

特性TLABPLAB
位置新生代Eden区老年代
用途新对象分配对象晋升
大小较小(几KB)较大(几KB到几MB)
频率极高中等
参数-XX:TLABSize动态调整

6. PLAB优势

性能提升:

java
1// 无PLAB:
2// 每次晋升都需要同步
3// 性能:1000次晋升/秒
4
5// 有PLAB:
6// 批量晋升,减少同步
7// 性能:10000次晋升/秒
8
9// 提升10倍

减少碎片:

1PLAB连续分配
2→ 减少内存碎片
3→ 提高老年代利用率

7. 相关参数

bash
1# 打印PLAB信息
2-XX:+PrintPLAB
3
4# 最小PLAB大小
5-XX:MinPLABSize=1024
6
7# 最大PLAB大小
8-XX:MaxPLABSize=1048576
9
10# PLAB浪费阈值
11-XX:PLABWasteTargetPercent=10

8. 实际应用

java
1// Young GC中的PLAB使用
2public class YoungGC {
3
4 public void evacuate() {
5 // 并行GC线程
6 parallelDo(() -> {
7 // 每个线程有自己的PLAB
8 PLAB plab = allocatePLAB();
9
10 // 扫描Survivor区
11 for (Object obj : survivor) {
12 if (obj.age >= threshold) {
13 // 晋升到老年代
14 promoteWithPLAB(obj, plab);
15 }
16 }
17 });
18 }
19
20 private void promoteWithPLAB(Object obj, PLAB plab) {
21 if (plab.canAllocate(obj.size())) {
22 // 在PLAB中分配
23 plab.allocate(obj);
24 } else {
25 // PLAB满,申请新的
26 plab = allocateNewPLAB();
27 plab.allocate(obj);
28 }
29 }
30}

9. 监控PLAB

bash
1# GC日志中的PLAB信息
2-XX:+PrintGCDetails -XX:+PrintPLAB
3
4# 输出示例
5[GC Worker #0: PLAB: 2048K, waste: 100K, refills: 3]
6[GC Worker #1: PLAB: 2048K, waste: 50K, refills: 2]
7[GC Worker #2: PLAB: 2048K, waste: 80K, refills: 4]
8
9# waste: PLAB浪费的空间
10# refills: PLAB重新分配次数

10. 优化建议

java
1// 1. 减少晋升频率
2// 增大新生代,减少Young GC频率
3-Xmn2g
4
5// 2. 提高晋升阈值
6// 让对象在新生代多停留几次
7-XX:MaxTenuringThreshold=15
8
9// 3. 避免大对象晋升
10// 大对象直接进老年代,不使用PLAB
11-XX:PretenureSizeThreshold=1048576 // 1MB
12
13// 4. 合理设置老年代大小
14// 确保有足够空间分配PLAB
15-Xmx4g

26. JVM 垃圾回收时产生的 concurrent mode failure 的原因是什么?

答案:

Concurrent Mode Failure是CMS并发收集期间老年代空间不足导致的失败。

主要原因:

  1. 并发期间晋升过快:Young GC频繁,对象快速晋升到老年代
  2. 触发时机过晚:CMSInitiatingOccupancyFraction设置过高
  3. 内存碎片:CMS使用标记-清除,产生碎片导致分配失败

后果:

  • 降级为Serial Old单线程Full GC
  • 应用暂停时间长(几秒)
  • 严重影响性能

解决方案:

bash
1# 提前触发CMS
2-XX:CMSInitiatingOccupancyFraction=70
3
4# 增大堆
5-Xmx4g
6
7# 或切换到G1
8-XX:+UseG1GC

27. 为什么 Java 中 CMS 垃圾收集器在发生 Concurrent Mode Failure 时的 Full GC 是单线程的?

答案:

CMS失败时降级为Serial Old收集器,这是历史设计决定。

原因:

  1. 历史设计:CMS设计时选择Serial Old作为后备
  2. 数据结构不兼容:CMS与Parallel Old数据结构不同
  3. 实现简单:Serial Old稳定可靠

性能影响:

1正常CMS:暂停50ms
2Serial Old Full GC:暂停5000ms(100倍差距)

解决方案:

bash
1# 切换到G1(多线程Full GC)
2-XX:+UseG1GC

28. 为什么 Java 中某些新生代和老年代的垃圾收集器不能组合使用?比如 ParNew 和 Parallel Old

答案:

收集器组合限制源于框架、数据结构和设计目标的不兼容。

可用组合:

1Serial -> Serial Old
2ParNew -> CMS
3Parallel -> Parallel Old
4G1 -> G1

不兼容原因:

  1. 框架不同:ParNew基于CMS框架,Parallel Old基于Parallel框架
  2. 数据结构不同:Card Table vs Region-based
  3. 设计目标不同:低延迟 vs 高吞吐量

推荐配置:

bash
1# 低延迟
2-XX:+UseG1GC
3
4# 高吞吐
5-XX:+UseParallelGC

29. JVM 新生代垃圾回收如何避免全堆扫描?

答案:

通过Card Table和写屏障技术避免扫描整个老年代。

Card Table原理:

java
1// 将老年代划分为512字节的Card
2// 用字节数组记录哪些Card有跨代引用
3byte[] cardTable;
4
5// 写屏障自动标记
6void writeBarrier(Object obj, Object ref) {
7 obj.field = ref;
8 if (isOld(obj) && isYoung(ref)) {
9 cardTable.markDirty(obj);
10 }
11}

Young GC流程:

java
1// 只扫描脏Card,不扫描整个老年代
2for (Card card : dirtyCards) {
3 scanCard(card);
4}

性能提升:

  • 从扫描几GB降到几MB
  • 性能提升100倍以上

30. Java 的 CMS 垃圾回收器和 G1 垃圾回收器在记忆集的维护上有什么不同?

答案:

CMS使用Card Table,G1使用Remembered Set,粒度和精度不同。

对比:

特性CMS Card TableG1 Remembered Set
粒度512字节Region(1-32MB)
记录内容是否有引用精确引用来源
空间开销0.2%5-10%
扫描效率中等
适用场景小堆大堆

CMS:

java
1// 简单标记Card为脏
2cardTable[index] = DIRTY;

G1:

java
1// 精确记录引用来源
2region.rset.addReference(fromRegion, card);

选择建议:

  • 小堆(小于4GB):CMS
  • 大堆(大于4GB):G1

31. 为什么 G1 垃圾收集器不维护年轻代到老年代的记忆集?

答案:

G1只维护老年代到年轻代的记忆集,因为年轻代总是会被完整收集。

原因分析:

1. 年轻代总是全部收集

java
1// G1的Young GC
2// 总是收集所有年轻代Region
3void youngGC() {
4 // 收集所有Eden Region
5 collectAllEdenRegions();
6
7 // 收集所有Survivor Region
8 collectAllSurvivorRegions();
9
10 // 无需记忆集,因为全部扫描
11}

2. 老年代部分收集

java
1// G1的Mixed GC
2// 只收集部分老年代Region
3void mixedGC() {
4 // 收集所有年轻代
5 collectYoungRegions();
6
7 // 只收集部分老年代
8 collectSelectedOldRegions();
9
10 // 需要RSet记录其他老年代Region的引用
11}

3. 记忆集的作用

1记忆集用于:
2- 避免扫描整个堆
3- 只扫描可能包含引用的区域
4
5年轻代→老年代:
6- 不需要RSet
7- 因为年轻代总是全部收集
8- 所有引用都会被扫描到
9
10老年代→年轻代:
11- 需要RSet
12- 因为只收集部分老年代
13- 需要知道哪些老年代Region引用了年轻代

4. 内存开销考虑

1如果维护年轻代→老年代RSet:
2- 额外的内存开销
3- 额外的维护成本
4- 没有实际收益(因为年轻代总是全收集)

32. Java 中的 CMS 和 G1 垃圾收集器如何维持并发的正确性?

答案:

CMS使用增量更新,G1使用SATB(原始快照),都通过写屏障维持并发正确性。

CMS增量更新:

java
1// 当黑色对象引用白色对象时
2void cmsWriteBarrier(Object obj, Object ref) {
3 if (obj.isBlack() && ref.isWhite()) {
4 // 将黑色对象重新标记为灰色
5 obj.setGray();
6 }
7 obj.field = ref;
8}

G1 SATB:

java
1// 记录删除的引用
2void g1WriteBarrier(Object obj, Object newRef) {
3 Object oldRef = obj.field;
4
5 // 记录旧引用
6 if (oldRef != null && isMarking) {
7 satbQueue.enqueue(oldRef);
8 }
9
10 obj.field = newRef;
11}

对比:

特性CMS增量更新G1 SATB
策略记录新增引用记录删除引用
重新标记工作量较大较小
浮动垃圾较少较多
实现复杂度简单复杂

33. Java G1 相对于 CMS 有哪些进步的地方?

答案:

G1在可预测性、内存碎片、大堆支持等方面都优于CMS。

主要进步:

1. 可预测的停顿时间

bash
1# G1可以设置停顿时间目标
2-XX:MaxGCPauseMillis=200
3
4# CMS无法精确控制停顿时间

2. 无内存碎片

1CMS:标记-清除,产生碎片
2G1:标记-整理,无碎片

3. 大堆支持更好

1CMS:适合小于8GB
2G1:适合8-64GB
3ZGC:大于64GB

4. 分代收集更灵活

1CMS:固定的新生代和老年代
2G1:动态调整Region角色

5. 避免Concurrent Mode Failure

1CMS:可能降级为Serial Old
2G1:使用Evacuation Failure机制,性能影响小

6. 整体吞吐量更高

1CMS:并发收集,但碎片影响性能
2G1:整理内存,长期性能更稳定

34. 什么是 Java 中的 logging write barrier?

答案:

Logging Write Barrier是G1用于维护Remembered Set的写屏障机制。

工作原理:

java
1// G1的写屏障
2void g1WriteBarrier(Object obj, Object newRef) {
3 // 1. SATB写屏障(并发标记期间)
4 if (isMarking) {
5 Object oldRef = obj.field;
6 if (oldRef != null) {
7 satbQueue.enqueue(oldRef);
8 }
9 }
10
11 // 2. 实际赋值
12 obj.field = newRef;
13
14 // 3. RSet维护写屏障
15 if (newRef != null) {
16 Region fromRegion = getRegion(obj);
17 Region toRegion = getRegion(newRef);
18
19 if (fromRegion != toRegion) {
20 // 记录到日志缓冲区
21 logBuffer.add(fromRegion, toRegion, getCard(obj));
22 }
23 }
24}
25
26// 后台线程处理日志
27void processLogBuffer() {
28 for (LogEntry entry : logBuffer) {
29 entry.toRegion.rset.add(entry.fromRegion, entry.card);
30 }
31}

优势:

  • 应用线程只记录日志,开销小
  • 后台线程异步更新RSet
  • 减少应用线程停顿

35. Java 的 G1 垃圾回收流程是怎样的?

答案:

G1包括Young GC、并发标记和Mixed GC三个主要阶段。

1. Young GC(频繁)

11. 选择所有年轻代Region
22. STW,复制存活对象
33. 部分对象晋升到老年代
44. 时间:10-50ms

2. 并发标记周期

1阶段1:初始标记(STW)
2- 标记GC Roots
3- 时间:几毫秒
4
5阶段2:并发标记
6- 并发遍历对象图
7- 使用SATB
8- 时间:几百毫秒
9
10阶段3:最终标记(STW)
11- 处理SATB队列
12- 时间:几十毫秒
13
14阶段4:清理(部分STW)
15- 统计Region存活率
16- 选择回收集合

3. Mixed GC

11. 收集所有年轻代Region
22. 收集部分老年代Region(垃圾最多的)
33. 根据停顿时间目标选择Region数量
44. 时间:可控制在MaxGCPauseMillis内

完整流程:

1Young GC → Young GC → ...
2 ↓(老年代占用达到45%)
3并发标记周期
4
5Mixed GC → Mixed GC → ...
6
7Young GC → ...

36. Java 的 CMS 垃圾回收流程是怎样的?

答案:

CMS分为4个阶段:初始标记、并发标记、重新标记、并发清除。

完整流程:

阶段1:初始标记(STW)

1- 标记GC Roots直接引用的对象
2- 时间短:几毫秒
3- 多线程执行

阶段2:并发标记

1- 与应用线程并发执行
2- 遍历对象图,标记可达对象
3- 使用增量更新
4- 时间长:几百毫秒

阶段3:重新标记(STW)

1- 处理并发标记期间的变化
2- 扫描Card Table
3- 时间:几十毫秒
4- 多线程执行

阶段4:并发清除

1- 与应用线程并发执行
2- 清除未标记的对象
3- 不移动对象,产生碎片
4- 时间:几百毫秒

时间线:

1初始标记(STW 5ms) → 并发标记(300ms) → 重新标记(STW 50ms) → 并发清除(200ms)

37. 你了解 Java 的 ZGC(Z Garbage Collector)吗?

答案:

ZGC是JDK 11引入的低延迟垃圾收集器,停顿时间小于10ms。

核心特性:

1. 超低延迟

1停顿时间:小于10ms
2与堆大小无关
3适用于大堆(大于64GB)

2. Colored Pointer

java
1// 64位指针布局
2[unused 16bit][Finalizable 1bit][Remapped 1bit][Marked1 1bit][Marked0 1bit][address 44bit]
3
4// 通过指针颜色记录对象状态
5// 无需额外内存开销

3. Load Barrier

java
1// 读屏障,在读取对象时触发
2void loadBarrier(Object obj) {
3 if (needsBarrier(obj)) {
4 // 重定位对象
5 obj = relocate(obj);
6 }
7 return obj;
8}

4. 并发收集

1所有阶段几乎都是并发的:
2- 并发标记
3- 并发重定位
4- 并发引用处理

使用:

bash
1-XX:+UseZGC
2-Xmx16g

38. JVM 垃圾回收调优的主要目标是什么?

答案:

GC调优目标是平衡延迟、吞吐量和内存占用。

三大目标:

1. 降低延迟

1- 减少GC停顿时间
2- 减少GC频率
3- 目标:GC停顿小于100ms

2. 提高吞吐量

1- 增加应用运行时间占比
2- 减少GC时间占比
3- 目标:GC时间小于5%

3. 控制内存占用

1- 合理设置堆大小
2- 避免内存浪费
3- 目标:内存利用率>70%

不可能三角:

1低延迟 ←→ 高吞吐量
2
3 低内存
4
5只能同时满足两个目标

39. 如何对 Java 的垃圾回收进行调优?

答案:

GC调优需要分析问题、设置参数、测试验证。

调优步骤:

1. 收集GC日志

bash
1-XX:+PrintGCDetails
2-XX:+PrintGCDateStamps
3-Xloggc:gc.log

2. 分析GC问题

1- Young GC频繁?增大新生代
2- Full GC频繁?增大老年代
3- GC时间长?切换收集器

3. 调整参数

bash
1# 堆大小
2-Xms4g -Xmx4g
3
4# 新生代大小
5-Xmn1g
6
7# 收集器
8-XX:+UseG1GC
9-XX:MaxGCPauseMillis=200

4. 验证效果

bash
1# 监控指标
2jstat -gcutil <pid> 1000

40. 常用的 JVM 配置参数有哪些?

答案:

内存配置:

bash
1-Xms4g # 初始堆大小
2-Xmx4g # 最大堆大小
3-Xmn1g # 新生代大小
4-Xss1m # 线程栈大小
5-XX:MetaspaceSize=256m # 元空间初始大小
6-XX:MaxMetaspaceSize=512m # 元空间最大大小

收集器选择:

bash
1-XX:+UseG1GC # 使用G1
2-XX:+UseZGC # 使用ZGC
3-XX:+UseConcMarkSweepGC # 使用CMS
4-XX:MaxGCPauseMillis=200 # 最大停顿时间

GC日志:

bash
1-XX:+PrintGCDetails
2-XX:+PrintGCDateStamps
3-Xloggc:gc.log
4-XX:+UseGCLogFileRotation
5-XX:NumberOfGCLogFiles=10
6-XX:GCLogFileSize=100M

OOM处理:

bash
1-XX:+HeapDumpOnOutOfMemoryError
2-XX:HeapDumpPath=/logs/heapdump.hprof
3-XX:OnOutOfMemoryError="sh /scripts/restart.sh"

性能调优:

bash
1-XX:+UseStringDeduplication # 字符串去重
2-XX:+UseTLAB # 线程本地分配缓冲

41. 你常用哪些工具来分析 JVM 性能?

答案:

命令行工具:

bash
1# jps - 查看Java进程
2jps -lvm
3
4# jstat - GC统计
5jstat -gcutil <pid> 1000
6
7# jmap - 堆转储
8jmap -dump:live,format=b,file=heap.bin <pid>
9
10# jstack - 线程堆栈
11jstack <pid> > thread.txt
12
13# jinfo - JVM参数
14jinfo -flags <pid>

图形化工具:

1- JConsole:实时监控
2- VisualVM:全面分析
3- JProfiler:性能分析
4- MAT:内存分析
5- GCViewer:GC日志分析

在线工具:

1- Arthas:阿里开源诊断工具
2- Async-profiler:CPU/内存分析

42. 如何在 Java 中进行内存泄漏分析?

答案:

分析步骤:

1. 获取堆转储

bash
1# 手动转储
2jmap -dump:live,format=b,file=heap.bin <pid>
3
4# 自动转储(OOM时)
5-XX:+HeapDumpOnOutOfMemoryError
6-XX:HeapDumpPath=/logs/heapdump.hprof

2. 使用MAT分析

11. 打开heap.bin
22. 查看Dominator Tree
33. 找到占用内存最多的对象
44. 分析GC Roots引用链
55. 定位泄漏代码

3. 常见泄漏场景

java
1// 场景1:静态集合
2public class Leak {
3 private static List<Object> list = new ArrayList<>();
4 public void add() {
5 list.add(new Object()); // 永不清理
6 }
7}
8
9// 场景2:ThreadLocal未清理
10ThreadLocal<byte[]> local = new ThreadLocal<>();
11local.set(new byte[1024 * 1024]);
12// 忘记local.remove()
13
14// 场景3:监听器未移除
15button.addListener(listener);
16// 忘记button.removeListener(listener)

43. Java 里的对象在虚拟机里面是怎么存储的?

答案:

Java对象在内存中分为对象头、实例数据和对齐填充三部分。

对象结构:

1[对象头][实例数据][对齐填充]

1. 对象头(Object Header)

1Mark Word(8字节):
2- 哈希码
3- GC分代年龄
4- 锁状态标志
5- 线程ID
6
7Class Pointer(4/8字节):
8- 指向类元数据
9- 开启指针压缩为4字节
10
11Array Length(4字节,仅数组):
12- 数组长度

2. 实例数据

java
1public class User {
2 private int age; // 4字节
3 private String name; // 4/8字节(引用)
4}

3. 对齐填充

1对象大小必须是8字节的倍数
2不足部分用填充补齐

示例计算:

java
1public class Point {
2 private int x; // 4字节
3 private int y; // 4字节
4}
5
6// 对象大小:
7// Mark Word: 8字节
8// Class Pointer: 4字节(压缩)
9// x: 4字节
10// y: 4字节
11// 总计: 20字节
12// 对齐后: 24字节

44. 说说 Java 的执行流程?

答案:

Java代码经过编译、加载、验证、执行多个阶段。

完整流程:

1. 编译阶段

1.java源文件
2 ↓ javac编译
3.class字节码文件

2. 类加载阶段

1加载(Loading)
2
3验证(Verification)
4
5准备(Preparation)
6
7解析(Resolution)
8
9初始化(Initialization)

3. 执行阶段

1字节码
2
3解释执行 / JIT编译
4
5本地机器码
6
7CPU执行

详细流程:

java
1// 1. 编写代码
2public class Hello {
3 public static void main(String[] args) {
4 System.out.println("Hello");
5 }
6}
7
8// 2. javac编译
9javac Hello.java // 生成Hello.class
10
11// 3. java运行
12java Hello
13
14// 4. JVM执行
15// - 类加载器加载Hello.class
16// - 验证字节码合法性
17// - 分配内存,初始化静态变量
18// - 执行main方法
19// - 解释执行或JIT编译
20// - 输出Hello

学习指南

核心要点:

  • JVM 内存模型和垃圾回收机制
  • 类加载过程和双亲委派模型
  • 性能监控和调优方法
  • 常见内存问题排查

学习路径建议:

  1. 掌握 JVM 内存结构和对象生命周期
  2. 深入理解垃圾回收算法和收集器
  3. 学习 JVM 调优参数和监控工具
  4. 掌握内存泄漏分析和性能优化
forum

评论区 / Comments