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工作原理
1// 对象分配流程2public Object allocate(int size) {3 // 1. 尝试在TLAB中分配4 if (tlab.canAllocate(size)) {5 return tlab.allocate(size);6 }7 8 // 2. TLAB空间不足,申请新的TLAB9 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参数
1# 启用TLAB(默认开启)2-XX:+UseTLAB34# 设置TLAB大小5-XX:TLABSize=256k67# TLAB占Eden区的比例8-XX:TLABWasteTargetPercent=1910# 打印TLAB信息11-XX:+PrintTLAB5. 优势
- 无锁分配:避免同步开销
- 减少碎片:连续分配
- 提高性能:分配速度快
3. Java 是如何实现跨平台的?
答案:
Java通过"一次编译,到处运行"的机制实现跨平台。
1. 核心机制
编译过程:
1Java源代码(.java) -> 编译器(javac) -> 字节码(.class)执行过程:
1字节码(.class) -> JVM -> 机器码 -> 操作系统2. 关键组件
字节码(Bytecode):
- 平台无关的中间代码
- JVM规范定义的指令集
- 所有平台的JVM都能识别
Java虚拟机(JVM):
- 平台相关的虚拟机实现
- 负责将字节码翻译成机器码
- 不同平台有不同的JVM实现
3. 实现原理
1// 示例代码2public class Hello {3 public static void main(String[] args) {4 System.out.println("Hello World");5 }6}78// 编译后的字节码(部分)9public static void main(java.lang.String[]);10 Code:11 0: getstatic #2 // Field java/lang/System.out12 3: ldc #3 // String Hello World13 5: invokevirtual #4 // Method java/io/PrintStream.println14 8: return4. 跨平台架构
1┌─────────────────────────────────────┐2│ Java Application │3├─────────────────────────────────────┤4│ Java API (rt.jar) │5├─────────────────────────────────────┤6│ JVM │7├──────────┬──────────┬───────────────┤8│ Windows │ Linux │ macOS │9└──────────┴──────────┴───────────────┘5. 平台差异处理
JNI(Java Native Interface):
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. 内存分布示例
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编译(激进优化)工作流程:
1// 1. 程序启动,解释执行2public void method() {3 // 解释器执行4}56// 2. 方法被频繁调用,触发JIT编译7// 调用次数达到阈值(默认10000次)8if (invocationCount > CompileThreshold) {9 // C1编译器编译(快速编译)10 compileWithC1();11}1213// 3. 继续频繁调用,触发C2编译14if (invocationCount > TierThreshold) {15 // C2编译器编译(深度优化)16 compileWithC2();17}4. 热点代码检测
方法调用计数器:
- 统计方法调用次数
- 达到阈值触发编译
回边计数器:
- 统计循环执行次数
- 检测循环热点
5. JVM参数配置
1# 只使用解释器2-Xint34# 只使用编译器(需要预热)5-Xcomp67# 混合模式(默认)8-Xmixed910# 设置编译阈值11-XX:CompileThreshold=100001213# 启用分层编译(默认开启)14-XX:+TieredCompilation1516# 打印编译信息17-XX:+PrintCompilation6. 性能对比
| 特性 | 解释执行 | 编译执行 | 混合模式 |
|---|---|---|---|
| 启动速度 | 快 | 慢 | 中等 |
| 执行速度 | 慢 | 快 | 快 |
| 内存占用 | 小 | 大 | 中等 |
| 优化程度 | 无 | 高 | 高 |
| 适用场景 | 短期运行 | 长期运行 | 通用 |
7. 实际应用
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:加载大量类
1// 动态生成大量类导致OOM2public 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:大量使用反射
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及之前(永久代):
1# 设置永久代大小2-XX:PermSize=64m3-XX:MaxPermSize=256m45# 错误信息6java.lang.OutOfMemoryError: PermGen spaceJDK 8及之后(元空间):
1# 设置元空间大小2-XX:MetaspaceSize=128m3-XX:MaxMetaspaceSize=512m45# 错误信息6java.lang.OutOfMemoryError: Metaspace4. 元空间优势
- 使用本地内存,不受堆大小限制
- 自动扩展,默认无上限
- 减少Full GC频率
5. 预防措施
1# 合理设置元空间大小2-XX:MetaspaceSize=256m3-XX:MaxMetaspaceSize=512m45# 监控元空间使用6-XX:+TraceClassLoading7-XX:+TraceClassUnloading7. Java 中堆和栈的区别是什么?
答案:
堆和栈是JVM内存中两个重要的区域,用途和特性完全不同。
1. 核心区别对比
| 特性 | 堆(Heap) | 栈(Stack) |
|---|---|---|
| 作用域 | 线程共享 | 线程私有 |
| 存储内容 | 对象实例、数组 | 局部变量、方法调用 |
| 生命周期 | 对象创建到GC回收 | 方法调用到返回 |
| 大小 | 较大(GB级) | 较小(MB级) |
| 分配方式 | 动态分配 | 连续分配 |
| 回收方式 | GC自动回收 | 自动弹出 |
| 异常 | OutOfMemoryError | StackOverflowError |
| 速度 | 较慢 | 很快 |
2. 内存结构
堆结构:
1Heap2├── Young Generation (新生代)3│ ├── Eden (伊甸区)4│ ├── Survivor 0 (S0)5│ └── Survivor 1 (S1)6└── Old Generation (老年代)栈结构:
1Stack2├── Stack Frame 1 (栈帧1)3│ ├── 局部变量表4│ ├── 操作数栈5│ ├── 动态链接6│ └── 返回地址7├── Stack Frame 2 (栈帧2)8└── ...3. 代码示例
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. 内存分配示例
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):
1// 不断创建对象2List<byte[]> list = new ArrayList<>();3while (true) {4 list.add(new byte[1024 * 1024]); // 1MB5}6// java.lang.OutOfMemoryError: Java heap space栈溢出(StackOverflowError):
1// 无限递归2public void recursion() {3 recursion();4}5// java.lang.StackOverflowError6. JVM参数配置
1# 堆配置2-Xms2g # 初始堆大小3-Xmx4g # 最大堆大小4-Xmn1g # 新生代大小56# 栈配置7-Xss1m # 每个线程栈大小8. 什么是 Java 中的直接内存(堆外内存)?
答案:
直接内存是JVM堆外的内存,不受JVM堆大小限制,主要用于NIO操作。
1. 什么是直接内存
定义:
- 操作系统的本地内存
- 不在JVM堆中
- 通过
DirectByteBuffer访问
2. 使用场景
NIO操作:
1// 分配直接内存2ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);34// 传统堆内存5ByteBuffer heapBuffer = ByteBuffer.allocate(1024);文件IO:
1// 使用直接内存进行文件读写2FileChannel channel = new RandomAccessFile("file.txt", "rw").getChannel();3ByteBuffer buffer = ByteBuffer.allocateDirect(1024);45// 读取6channel.read(buffer);78// 写入9buffer.flip();10channel.write(buffer);3. 直接内存优势
零拷贝:
1传统IO:2磁盘 -> 内核缓冲区 -> JVM堆 -> 内核缓冲区 -> Socket3(4次拷贝)45直接内存:6磁盘 -> 内核缓冲区 -> 直接内存 -> Socket7(2次拷贝)减少GC压力:
- 不在堆中,不参与GC
- 适合大数据量操作
4. 直接内存管理
分配:
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}手动释放:
1// 使用Unsafe释放2public static void freeDirectBuffer(ByteBuffer buffer) {3 if (buffer instanceof DirectBuffer) {4 ((DirectBuffer) buffer).cleaner().clean();5 }6}5. 内存溢出
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 memory6. JVM参数配置
1# 设置直接内存最大值2-XX:MaxDirectMemorySize=512m34# 不设置则默认等于-Xmx7. 监控直接内存
1// 获取直接内存使用情况2import sun.misc.SharedSecrets;34long maxDirectMemory = sun.misc.VM.maxDirectMemory();5long usedDirectMemory = SharedSecrets.getJavaNioAccess()6 .getDirectBufferPool().getMemoryUsed();78System.out.println("最大直接内存: " + maxDirectMemory);9System.out.println("已用直接内存: " + usedDirectMemory);8. 使用建议
适用场景:
- 大文件IO
- 网络通信
- 频繁IO操作
注意事项:
- 分配和释放成本高
- 不受GC管理,需手动释放
- 可能导致内存泄漏
9. 什么是 Java 中的常量池?
答案:
Java中有三种常量池:Class文件常量池、运行时常量池和字符串常量池。
1. Class文件常量池
位置:
- .class文件中
- 编译期生成
内容:
- 字面量(字符串、final常量)
- 符号引用(类、方法、字段)
查看:
1# 查看class文件常量池2javap -v ClassName.class2. 运行时常量池
位置:
- JDK 7之前:方法区(永久代)
- JDK 7及之后:堆中
特点:
- 类加载时从Class文件常量池加载
- 动态性(可运行期添加)
3. 字符串常量池
位置:
- JDK 7之前:永久代
- JDK 7及之后:堆中
作用:
- 避免重复创建字符串
- 节省内存
4. 字符串常量池详解
示例1:字面量
1String s1 = "hello"; // 在常量池创建2String s2 = "hello"; // 直接引用常量池3System.out.println(s1 == s2); // true示例2:new String()
1String s1 = new String("hello"); // 堆中创建对象2String s2 = "hello"; // 常量池3System.out.println(s1 == s2); // false45String s3 = s1.intern(); // 返回常量池引用6System.out.println(s2 == s3); // true示例3:字符串拼接
1// 编译期确定,放入常量池2String s1 = "hello" + "world";3String s2 = "helloworld";4System.out.println(s1 == s2); // true56// 运行期确定,在堆中创建7String s3 = "hello";8String s4 = s3 + "world";9String s5 = "helloworld";10System.out.println(s4 == s5); // false5. intern()方法
JDK 6:
1String s1 = new String("hello");2String s2 = s1.intern();3String s3 = "hello";4System.out.println(s2 == s3); // true5// intern()将字符串复制到永久代常量池JDK 7+:
1String s1 = new String("he") + new String("llo");2String s2 = s1.intern();3String s3 = "hello";4System.out.println(s1 == s3); // true5// intern()将堆中字符串引用放入常量池6. 常量池大小配置
1# JDK 6/7 设置字符串常量池大小2-XX:StringTableSize=100000334# JDK 8+ 默认600135-XX:StringTableSize=10000037. 实际应用
优化内存:
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. 双亲委派模型
工作流程:
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,委派给Bootstrap11 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. 双亲委派优势
避免重复加载:
- 父加载器已加载,子加载器不再加载
安全性:
1// 自定义java.lang.String会被拒绝2// 因为Bootstrap已加载核心String类3public class String {4 // 这个类不会被加载5}5. 自定义类加载器
1public class MyClassLoader extends ClassLoader {2 3 private String classPath;4 5 public MyClassLoader(String classPath) {6 this.classPath = classPath;7 }8 9 @Override10 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}4041// 使用42MyClassLoader loader = new MyClassLoader("/custom/path");43Class<?> clazz = loader.loadClass("com.example.MyClass");44Object obj = clazz.newInstance();6. 破坏双亲委派
场景1:JDBC驱动加载
1// 使用线程上下文类加载器2Thread.currentThread().setContextClassLoader(customLoader);场景2:OSGi模块化
- 每个模块独立类加载器
- 平级委派,不遵循双亲委派
场景3:热部署
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. 工作原理
1// 执行流程2public void hotMethod() {3 // 初始:解释执行4 // 调用次数++5 6 // 达到阈值(默认10000次)7 if (invocationCount >= CompileThreshold) {8 // 触发JIT编译9 compileToNativeCode();10 }11 12 // 编译后:直接执行机器码13}3. 热点检测
方法调用计数器:
1// 统计方法调用次数2int invocationCount = 0;34public void method() {5 invocationCount++;6 if (invocationCount > threshold) {7 // 触发编译8 }9}回边计数器:
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优化技术
方法内联:
1// 优化前2public int add(int a, int b) {3 return a + b;4}56public void test() {7 int result = add(1, 2);8}910// 优化后(内联)11public void test() {12 int result = 1 + 2; // 直接内联13}逃逸分析:
1// 对象未逃逸,可在栈上分配2public void method() {3 Point p = new Point(1, 2);4 int x = p.getX(); // 对象不会逃逸出方法5}循环展开:
1// 优化前2for (int i = 0; i < 4; i++) {3 sum += arr[i];4}56// 优化后7sum += arr[0];8sum += arr[1];9sum += arr[2];10sum += arr[3];6. JVM参数
1# 设置编译阈值2-XX:CompileThreshold=1000034# 禁用JIT5-Xint67# 只用编译模式8-Xcomp910# 打印编译信息11-XX:+PrintCompilation1213# 打印内联信息14-XX:+PrintInlining7. 性能提升
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结构
1CodeCache2├── Non-nmethods (非方法代码)3│ └── JVM内部代码4├── Profiled nmethods (带profiling的代码)5│ └── C1编译的代码6└── Non-profiled nmethods (不带profiling的代码)7 └── C2编译的代码3. CodeCache大小配置
1# 设置CodeCache大小2-XX:ReservedCodeCacheSize=256m34# 初始大小5-XX:InitialCodeCacheSize=128m67# 打印CodeCache使用情况8-XX:+PrintCodeCache4. 查看CodeCache使用
1// 运行时查看2import sun.management.ManagementFactoryHelper;34MemoryPoolMXBean codeCache = ManagementFactoryHelper5 .getMemoryPools()6 .stream()7 .filter(pool -> pool.getName().contains("Code Cache"))8 .findFirst()9 .orElse(null);1011if (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满的影响
1// CodeCache满时的警告2Java HotSpot(TM) 64-Bit Server VM warning: 3CodeCache is full. Compiler has been disabled.45// 后果:6// 1. JIT编译停止7// 2. 只能解释执行8// 3. 性能严重下降6. 代码淘汰机制
1// 不常用的编译代码会被淘汰2// 释放CodeCache空间3// 需要时重新编译7. 监控CodeCache
1# 使用jconsole查看2# Memory -> Code Cache34# 使用jstat5jstat -compiler <pid>67# 输出编译统计8Compiled Failed Invalid Time FailedType FailedMethod91234 0 0 12.34 013. 什么是 Java 的 AOT(Ahead-Of-Time)?
答案:
AOT是提前编译技术,在程序运行前将字节码编译成本地机器码。
1. AOT vs JIT
| 特性 | AOT | JIT |
|---|---|---|
| 编译时机 | 运行前 | 运行时 |
| 启动速度 | 快 | 慢 |
| 峰值性能 | 中等 | 高 |
| 内存占用 | 小 | 大 |
| 优化程度 | 静态优化 | 动态优化 |
2. Java AOT实现
JDK 9引入:
1# 使用jaotc编译2jaotc --output libHelloWorld.so HelloWorld.class34# 运行时加载5java -XX:AOTLibrary=./libHelloWorld.so HelloWorldGraalVM Native Image:
1# 编译成原生可执行文件2native-image -jar application.jar34# 生成可执行文件5./application3. AOT优势
快速启动:
1// JIT启动时间:2-3秒2// AOT启动时间:0.1秒低内存占用:
- 无需JIT编译器
- 无需CodeCache
- 适合容器环境
4. AOT劣势
性能上限:
- 缺少运行时信息
- 无法做激进优化
- 峰值性能低于JIT
文件体积:
- 包含所有依赖
- 可执行文件较大
5. 使用场景
适合AOT:
- 微服务
- Serverless
- 容器应用
- 命令行工具
适合JIT:
- 长期运行的服务
- 需要峰值性能
- 复杂业务逻辑
6. GraalVM示例
1# 安装GraalVM2sdk install java 21.0.0.r11-grl34# 编译Spring Boot应用5./mvnw package -Pnative67# 生成原生镜像8native-image -jar target/app.jar910# 运行11./app12# 启动时间:小于100ms13# 内存占用:小于50MB7. 配置参数
1# 启用AOT2-XX:+UseAOT34# 指定AOT库5-XX:AOTLibrary=./lib.so67# 打印AOT信息8-XX:+PrintAOT14. 你了解 Java 的逃逸分析吗?
答案:
逃逸分析是JVM的一种优化技术,分析对象的作用域,进行栈上分配、标量替换等优化。
1. 什么是逃逸
对象逃逸:
- 对象在方法外被引用
- 对象被外部访问
未逃逸:
- 对象只在方法内使用
- 不会被外部访问
2. 逃逸类型
方法逃逸:
1// 对象逃逸出方法2public User createUser() {3 User user = new User();4 return user; // 逃逸5}线程逃逸:
1// 对象被其他线程访问2private User user;34public void method() {5 this.user = new User(); // 线程逃逸6}未逃逸:
1// 对象不逃逸2public void method() {3 User user = new User();4 user.setName("Tom");5 System.out.println(user.getName());6 // user不会逃逸出方法7}3. 逃逸分析优化
栈上分配:
1// 优化前:对象在堆上分配2public void method() {3 Point p = new Point(1, 2); // 堆分配4 int x = p.getX();5}67// 优化后:对象在栈上分配8public void method() {9 // 栈上分配,方法结束自动回收10 // 无需GC11}标量替换:
1// 优化前2public void method() {3 Point p = new Point(1, 2);4 int sum = p.x + p.y;5}67// 优化后:对象被拆解为标量8public void method() {9 int x = 1;10 int y = 2;11 int sum = x + y; // 直接使用标量12}锁消除:
1// 优化前2public void method() {3 StringBuffer sb = new StringBuffer();4 sb.append("a"); // 内部有synchronized5 sb.append("b");6}78// 优化后:sb未逃逸,消除锁9public void method() {10 StringBuffer sb = new StringBuffer();11 sb.append("a"); // 去除synchronized12 sb.append("b");13}4. 性能对比
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参数
1# 启用逃逸分析(默认开启)2-XX:+DoEscapeAnalysis34# 启用标量替换5-XX:+EliminateAllocations67# 启用锁消除8-XX:+EliminateLocks910# 打印逃逸分析结果11-XX:+PrintEscapeAnalysis6. 实际应用
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)
定义:
- 最常见的引用
- 只要强引用存在,对象不会被回收
示例:
1// 强引用2Object obj = new Object();34// 只要obj存在,对象不会被GC5// 即使内存不足也不会回收6// 除非obj = null特点:
- 宁可OOM也不回收
- 最常用的引用类型
2. 软引用(Soft Reference)
定义:
- 内存不足时才回收
- 适合缓存场景
示例:
1// 创建软引用2SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024 * 1024]);34// 获取对象5byte[] data = softRef.get();6if (data != null) {7 // 对象还在8} else {9 // 对象已被回收10}应用场景:
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时必定回收
- 生命周期更短
示例:
1// 创建弱引用2WeakReference<Object> weakRef = new WeakReference<>(new Object());34// 获取对象5Object obj = weakRef.get();6if (obj != null) {7 // 对象还在8}910// GC后11System.gc();12obj = weakRef.get(); // null应用场景:
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}1213// WeakHashMap14WeakHashMap<Key, Value> map = new WeakHashMap<>();15Key key = new Key();16map.put(key, value);1718key = null; // 去除强引用19System.gc(); // GC后,map中的entry被自动移除4. 虚引用(Phantom Reference)
定义:
- 最弱的引用
- 无法通过get()获取对象
- 用于跟踪对象回收
示例:
1// 创建虚引用(必须配合引用队列)2ReferenceQueue<Object> queue = new ReferenceQueue<>();3PhantomReference<Object> phantomRef = new PhantomReference<>(4 new Object(), queue5);67// get()永远返回null8Object obj = phantomRef.get(); // null910// 对象被回收时,虚引用会进入队列11Reference<?> ref = queue.poll();12if (ref != null) {13 // 对象已被回收,执行清理工作14}应用场景:
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. 引用队列
1// 配合引用队列使用2ReferenceQueue<Object> queue = new ReferenceQueue<>();34SoftReference<Object> softRef = new SoftReference<>(new Object(), queue);5WeakReference<Object> weakRef = new WeakReference<>(new Object(), queue);67// 对象被回收后,引用会进入队列8System.gc();910Reference<?> ref = queue.poll();11if (ref != null) {12 // 执行清理工作13 System.out.println("对象已被回收");14}7. 实际应用建议
1// 1. 普通对象:使用强引用2Object obj = new Object();34// 2. 缓存:使用软引用5SoftReference<Cache> cache = new SoftReference<>(new Cache());67// 3. 防止内存泄漏:使用弱引用8WeakHashMap<Key, Value> map = new WeakHashMap<>();910// 4. 监控回收:使用虚引用11PhantomReference<Resource> ref = new PhantomReference<>(resource, queue);16. Java 中常见的垃圾收集器有哪些?
答案:
Java提供多种垃圾收集器,适用于不同场景。
1. Serial收集器
特点:
- 单线程收集
- 简单高效
- 适合单核CPU
使用场景:
- 客户端应用
- 小内存环境
参数:
1# 新生代使用Serial2-XX:+UseSerialGC2. ParNew收集器
特点:
- Serial的多线程版本
- 新生代收集器
- 配合CMS使用
参数:
1-XX:+UseParNewGC3. Parallel Scavenge收集器
特点:
- 关注吞吐量
- 自适应调节
- 适合后台计算
参数:
1-XX:+UseParallelGC2# 设置吞吐量目标3-XX:GCTimeRatio=994# 最大暂停时间5-XX:MaxGCPauseMillis=1004. CMS收集器(Concurrent Mark Sweep)
特点:
- 并发收集
- 低停顿
- 适合响应时间敏感应用
阶段:
11. 初始标记(STW)22. 并发标记33. 重新标记(STW)44. 并发清除参数:
1-XX:+UseConcMarkSweepGC2# 触发GC的堆使用率3-XX:CMSInitiatingOccupancyFraction=70缺点:
- 产生内存碎片
- 并发模式失败
- CPU资源敏感
5. G1收集器(Garbage First)
特点:
- 面向服务端
- 可预测停顿
- 整堆收集
- 无内存碎片
内存布局:
1Heap划分为多个Region2每个Region可以是Eden、Survivor或Old参数:
1-XX:+UseG1GC2# 期望停顿时间3-XX:MaxGCPauseMillis=2004# 堆大小5-Xmx4g6. ZGC收集器
特点:
- 超低延迟(小于10ms)
- 支持TB级堆
- 并发收集
参数:
1-XX:+UseZGC2-Xmx16g7. Shenandoah GC
特点:
- 低停顿
- 并发整理
- 与堆大小无关的停顿时间
8. 收集器组合
| 新生代 | 老年代 | 说明 |
|---|---|---|
| Serial | Serial Old | 单线程 |
| ParNew | CMS | 低延迟 |
| Parallel Scavenge | Parallel Old | 高吞吐 |
| G1 | G1 | 平衡 |
9. 选择建议
小应用(小于100MB):
- Serial GC
中等应用(几GB):
- G1 GC
大内存应用(>10GB):
- ZGC或Shenandoah
低延迟要求:
- CMS或G1
高吞吐量要求:
- Parallel GC
17. Java 中如何判断对象是否是垃圾?不同实现方式有何区别?
答案:
Java主要使用可达性分析算法判断对象是否为垃圾。
1. 引用计数法(Java未采用)
原理:
- 为对象添加引用计数器
- 引用+1,失效-1
- 计数为0时回收
优点:
- 实现简单
- 判定高效
缺点:
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 // 两个对象互相引用,计数不为016 // 但实际已成为垃圾17 }18}2. 可达性分析算法(Java采用)
原理:
- 从GC Roots开始向下搜索
- 不可达的对象即为垃圾
GC Roots包括:
1// 1. 虚拟机栈中的引用2public void method() {3 Object obj = new Object(); // 栈中引用4}56// 2. 方法区中的静态变量7public class Test {8 private static Object staticObj = new Object();9}1011// 3. 方法区中的常量12public class Test {13 private static final Object CONSTANT = new Object();14}1516// 4. 本地方法栈中的引用17native void nativeMethod();1819// 5. 活跃线程20Thread thread = new Thread();可达性分析过程:
1GC Roots2 |3 ├─> Object A (可达)4 │ |5 │ └─> Object B (可达)6 |7 └─> Object C (可达)89Object D (不可达,垃圾)10 |11 └─> Object E (不可达,垃圾)3. 对象的生死判定
第一次标记:
1// 不可达对象被第一次标记2// 判断是否需要执行finalize()3if (!obj.isReachable() && obj.hasFinalize()) {4 // 放入F-Queue队列5 fQueue.add(obj);6}finalize()方法:
1public class FinalizeTest {2 public static FinalizeTest instance;3 4 @Override5 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 // 第一次GC15 instance = null;16 System.gc();17 Thread.sleep(500);18 19 if (instance != null) {20 System.out.println("对象存活"); // 自救成功21 }22 23 // 第二次GC24 instance = null;25 System.gc();26 Thread.sleep(500);27 28 if (instance == null) {29 System.out.println("对象死亡"); // finalize只执行一次30 }31 }32}4. 引用类型判定
1// 强引用:永不回收2Object obj = new Object();34// 软引用:内存不足时回收5SoftReference<Object> soft = new SoftReference<>(new Object());67// 弱引用:GC时回收8WeakReference<Object> weak = new WeakReference<>(new Object());910// 虚引用:无法获取对象11PhantomReference<Object> phantom = new PhantomReference<>(new Object(), queue);5. 方法区回收
类卸载条件:
1// 1. 该类所有实例已被回收2// 2. 加载该类的ClassLoader已被回收3// 3. 该类的Class对象没有被引用45// 示例:动态代理类可能被卸载6Proxy.newProxyInstance(...);18. 为什么 Java 的垃圾收集器将堆分为老年代和新生代?
答案:
分代收集基于两个假说,针对不同特征的对象采用不同的回收策略。
1. 分代假说
弱分代假说:
- 大部分对象朝生夕灭
- 98%的对象在创建后很快死亡
强分代假说:
- 熬过多次GC的对象难以消亡
- 存活时间长的对象会继续存活
2. 分代结构
1Java Heap2├── Young Generation (新生代)3│ ├── Eden (80%)4│ ├── Survivor 0 (10%)5│ └── Survivor 1 (10%)6└── Old Generation (老年代)3. 分代优势
针对性回收:
1// 新生代:频繁GC,快速回收2// 大部分对象在这里死亡3for (int i = 0; i < 1000000; i++) {4 Object temp = new Object(); // 临时对象5 // 方法结束后成为垃圾6}78// 老年代:少量GC,长期存活9public class Service {10 private static Cache cache = new Cache(); // 长期存活11}提高效率:
1新生代GC(Minor GC):2- 频率高3- 速度快(复制算法)4- 停顿时间短56老年代GC(Major GC):7- 频率低8- 速度慢(标记-整理)9- 停顿时间长4. 对象分配流程
1// 1. 新对象在Eden区分配2Object obj = new Object();34// 2. Eden区满,触发Minor GC5// 存活对象复制到Survivor67// 3. 多次GC后仍存活,晋升到老年代8// 默认15次(-XX:MaxTenuringThreshold=15)910// 4. 大对象直接进入老年代11byte[] bigObj = new byte[10 * 1024 * 1024]; // 10MB5. 性能对比
不分代的问题:
1每次GC都扫描整个堆2→ 效率低下3→ 停顿时间长分代的优势:
1Minor GC只扫描新生代2→ 扫描范围小3→ 速度快4→ 停顿时间短6. 实际数据
1新生代对象存活率:1-2%2老年代对象存活率:>90%34Minor GC频率:秒级5Major GC频率:分钟级67Minor GC耗时:几毫秒8Major GC耗时:几百毫秒19. 为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
答案:
元空间解决了永久代的诸多问题,提供更灵活的内存管理。
1. 永久代的问题
问题1:大小难以确定
1# JDK 7永久代配置2-XX:PermSize=64m3-XX:MaxPermSize=256m45# 问题:6# - 设置太小:容易OOM7# - 设置太大:浪费内存8# - 难以预估合适大小问题2:容易OOM
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 GC2→ 停顿时间长3→ 影响应用性能问题4:内存碎片
1永久代使用堆内存2→ 产生碎片3→ 影响分配效率2. 元空间的优势
优势1:使用本地内存
1# JDK 8元空间配置2-XX:MetaspaceSize=128m3-XX:MaxMetaspaceSize=512m45# 优势:6# - 不占用堆内存7# - 理论上只受物理内存限制8# - 默认无上限(自动扩展)优势2:自动扩展
1// 元空间会根据需要自动扩展2// 无需精确设置大小3// 减少OOM风险优势3:GC效率提升
1元空间GC独立进行2→ 不影响堆GC3→ 减少Full GC频率优势4:简化调优
1# 永久代:需要精确调优2-XX:PermSize=128m3-XX:MaxPermSize=256m45# 元空间:通常只设置最大值6-XX:MaxMetaspaceSize=512m3. 内存布局变化
JDK 7:
1Heap2├── Young Generation3├── Old Generation4└── Permanent Generation5 ├── 类元数据6 ├── 字符串常量池7 └── 静态变量JDK 8:
1Heap2├── Young Generation3├── Old Generation4└── 字符串常量池(移到堆)56Metaspace(本地内存)7└── 类元数据4. 迁移影响
字符串常量池迁移:
1// JDK 7+:字符串常量池在堆中2String s1 = "hello";3String s2 = new String("hello");4String s3 = s2.intern();56// s1和s3指向堆中的常量池7System.out.println(s1 == s3); // true静态变量迁移:
1// JDK 8:静态变量随Class对象存储在堆中2public class Test {3 private static Object obj = new Object();4 // obj存储在堆中5}5. 实际案例
永久代OOM:
1// JDK 72// 大量使用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元空间自动扩展:
1// JDK 82// 相同代码不会轻易OOM3// 元空间自动扩展4// 除非达到MaxMetaspaceSize或物理内存限制6. 监控对比
1# JDK 7监控永久代2jstat -gcpermcapacity <pid>34# JDK 8监控元空间5jstat -gcmetacapacity <pid>20. 为什么 Java 新生代被划分为 S0、S1 和 Eden 区?
答案:
新生代的三区划分是为了高效实现复制算法,避免内存碎片。
1. 新生代结构
1Young Generation2├── Eden (80%)3├── Survivor 0 (10%)4└── Survivor 1 (10%)56默认比例:8:1:12. 为什么需要两个Survivor
问题:只有一个Survivor
1Eden → Survivor → Old23问题:41. 内存碎片52. 无法区分不同年龄的对象63. 晋升策略难以实现解决:使用两个Survivor
1Eden + S0 → S12Eden + S1 → S034优势:51. 无内存碎片(复制算法)62. 对象年龄递增73. 灵活的晋升策略3. Minor GC流程
第一次GC:
1// 初始状态2Eden: [对象A, B, C, D, E]3S0: []4S1: []56// GC后(假设A、B存活)7Eden: []8S0: [A(age=1), B(age=1)]9S1: []第二次GC:
1// GC前2Eden: [对象F, G, H]3S0: [A(age=1), B(age=1)]4S1: []56// GC后(假设A、F存活)7Eden: []8S0: []9S1: [A(age=2), F(age=1)]多次GC后:
1// 对象年龄达到阈值(默认15)2if (age >= MaxTenuringThreshold) {3 // 晋升到老年代4 moveToOld(obj);5}4. 为什么是8:1:1比例
依据:
1新生代对象存活率:1-2%2Eden区:80%3Survivor:10% × 2 = 20%45每次GC:6- Eden + 1个Survivor(90%)7- 存活对象复制到另一个Survivor8- 10%空间足够容纳存活对象内存利用率:
1可用空间:Eden + 1个Survivor = 90%2浪费空间:1个Survivor = 10%34对比传统复制算法(50%利用率)5→ 大幅提升内存利用率5. 空间分配担保
1// 如果Survivor空间不足2if (survivorSize < aliveObjects) {3 // 直接晋升到老年代4 moveToOld(objects);5}67// 老年代空间检查8if (oldGenFreeSpace < youngGenUsedSpace) {9 // 触发Full GC10 fullGC();11}6. 实际示例
1public class GCTest {2 public static void main(String[] args) {3 // 1. 对象在Eden分配4 byte[] obj1 = new byte[1024 * 1024]; // 1MB5 6 // 2. Eden满,触发Minor GC7 // obj1存活,复制到S08 9 // 3. 继续分配10 byte[] obj2 = new byte[1024 * 1024];11 12 // 4. 再次GC13 // obj1复制到S1,age=214 // obj2复制到S1,age=115 16 // 5. 15次GC后17 // obj1晋升到老年代18 }19}7. 参数配置
1# 设置新生代大小2-Xmn512m34# 设置Eden和Survivor比例5-XX:SurvivorRatio=8 # Eden:Survivor = 8:167# 设置晋升年龄阈值8-XX:MaxTenuringThreshold=15910# 打印GC详情11-XX:+PrintGCDetails8. 优化建议
1// 1. 避免创建大对象2byte[] big = new byte[10 * 1024 * 1024]; // 直接进老年代34// 2. 复用对象5StringBuilder sb = new StringBuilder(); // 复用67// 3. 及时释放引用8list.clear(); // 帮助GC9list = null;21. 什么是三色标记算法?
答案:
三色标记算法是并发GC中用于标记对象的核心算法,解决并发标记时的对象漏标问题。
1. 三色定义
白色(White):
- 未被访问的对象
- 标记结束后仍为白色的对象会被回收
灰色(Gray):
- 已被访问但其引用的对象未全部访问
- 处于待处理状态
黑色(Black):
- 已被访问且其引用的对象也已访问
- 不会被回收
2. 标记过程
1// 初始状态:所有对象为白色2初始:所有对象 = 白色34// 从GC Roots开始标记5GC Roots -> 灰色67// 标记过程8while (存在灰色对象) {9 取出一个灰色对象10 标记为黑色11 将其引用的白色对象标记为灰色12}1314// 结束:白色对象即为垃圾3. 标记示例
1初始状态:2GC Root -> A -> B -> C3所有对象:白色45第1步:标记GC Root引用的A6A: 灰色7B, C: 白色89第2步:处理A,标记B10A: 黑色11B: 灰色12C: 白色1314第3步:处理B,标记C15A, B: 黑色16C: 灰色1718第4步:处理C19A, B, C: 黑色2021结束:白色对象被回收4. 并发标记问题
对象漏标问题:
1// 并发标记时,应用线程修改引用2初始状态:3A(黑) -> B(灰) -> C(白)45// 应用线程执行:6A.ref = C; // A引用C7B.ref = null; // B不再引用C89结果:10A(黑) -> C(白) // C被漏标!11B(灰)1213// C应该存活但被标记为白色14// 会被错误回收漏标条件(同时满足):
- 黑色对象指向白色对象
- 灰色对象到白色对象的引用被删除
5. 解决方案
增量更新(Incremental Update):
1// CMS使用2// 当黑色对象引用白色对象时3if (black.ref = white) {4 // 将黑色对象重新标记为灰色5 black -> 灰色6}78// 破坏条件1原始快照(SATB - Snapshot At The Beginning):
1// G1使用2// 当删除引用时3if (gray.ref = null) {4 // 记录被删除的引用5 记录(gray -> white)6}78// 破坏条件26. 写屏障实现
增量更新写屏障:
1// 伪代码2void writeBarrier(Object obj, Object ref) {3 if (obj.isBlack() && ref.isWhite()) {4 // 重新标记为灰色5 obj.setGray();6 }7 obj.ref = ref;8}SATB写屏障:
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 |
|---|---|---|
| 使用者 | CMS | G1 |
| 重新标记工作量 | 较大 | 较小 |
| 浮动垃圾 | 较少 | 较多 |
| 实现复杂度 | 简单 | 复杂 |
22. Java 中的 young GC、old GC、full GC 和 mixed GC 的区别是什么?
答案:
不同类型的GC针对不同的内存区域,有不同的触发条件和回收策略。
1. Young GC(Minor GC)
定义:
- 只回收新生代
- 最频繁的GC类型
触发条件:
1// Eden区满时触发2if (eden.isFull()) {3 youngGC();4}回收过程:
11. Eden区存活对象 -> Survivor22. Survivor区存活对象 -> 另一个Survivor33. 年龄达标对象 -> 老年代44. 清空Eden和一个Survivor特点:
- 频率高(秒级)
- 速度快(几毫秒)
- 使用复制算法
- STW时间短
2. Old GC(Major GC)
定义:
- 只回收老年代
- 较少使用的术语
触发条件:
1// 老年代空间不足2if (oldGen.freeSpace < threshold) {3 oldGC();4}特点:
- 频率低
- 速度慢
- 使用标记-整理算法
注意:
- Major GC通常伴随Full GC
- 很多情况下两者等价
3. Full GC
定义:
- 回收整个堆(新生代+老年代)
- 回收方法区
触发条件:
1// 1. 老年代空间不足2if (oldGen.freeSpace < requiredSpace) {3 fullGC();4}56// 2. 方法区空间不足7if (metaspace.isFull()) {8 fullGC();9}1011// 3. 空间分配担保失败12if (promotionFailed) {13 fullGC();14}1516// 4. 显式调用System.gc()17System.gc(); // 建议JVM执行Full GC1819// 5. CMS并发失败20if (concurrentModeFailure) {21 fullGC();22}特点:
- 频率最低
- 耗时最长(几百毫秒到几秒)
- STW时间长
- 影响应用性能
4. Mixed GC
定义:
- G1特有的GC类型
- 回收新生代+部分老年代
触发条件:
1// 老年代占用达到阈值2if (oldGenOccupancy > InitiatingHeapOccupancyPercent) {3 mixedGC();4}56// 默认45%7-XX:InitiatingHeapOccupancyPercent=45回收过程:
11. 回收所有新生代Region22. 回收部分老年代Region(价值最高的)33. 根据停顿时间目标选择Region数量特点:
- G1独有
- 可预测停顿时间
- 增量回收老年代
5. GC类型对比
| GC类型 | 回收区域 | 频率 | 耗时 | STW |
|---|---|---|---|---|
| Young GC | 新生代 | 高 | 短 | 短 |
| Old GC | 老年代 | 低 | 长 | 长 |
| Full GC | 整个堆 | 最低 | 最长 | 最长 |
| Mixed GC | 新生代+部分老年代 | 中 | 中 | 可控 |
6. 实际案例
Young GC示例:
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]; // 1MB5 // 频繁触发Young GC6 }7 }8}9// 日志:[GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] ...Full GC示例:
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]); // 1MB6 // 最终触发Full GC7 }8 }9}10// 日志:[Full GC (Ergonomics) [PSYoungGen: 2048K->0K] [ParOldGen: 8192K->8000K] ...7. 监控GC
1# 打印GC日志2-XX:+PrintGCDetails3-XX:+PrintGCDateStamps45# 使用jstat监控6jstat -gc <pid> 100078# 输出示例9S0C S1C S0U S1U EC EU OC OU YGC YGCT FGC FGCT102048.0 2048.0 0.0 512.0 16384.0 8192.0 40960.0 20480.0 100 0.500 5 2.00023. 什么条件会触发 Java 的 young GC?
答案:
Young GC主要在Eden区空间不足时触发。
1. 主要触发条件
Eden区满:
1// 最常见的触发条件2if (eden.freeSpace < objectSize) {3 // 触发Young GC4 youngGC();5}2. 对象分配流程
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 GC13 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. 触发时机示例
1public class YoungGCTrigger {2 public static void main(String[] args) {3 // 假设Eden区大小为10MB4 5 // 分配9MB,未触发GC6 byte[] obj1 = new byte[9 * 1024 * 1024];7 8 // 分配2MB,Eden不足,触发Young GC9 byte[] obj2 = new byte[2 * 1024 * 1024];10 11 // GC日志:12 // [GC (Allocation Failure) [PSYoungGen: 9216K->1024K(10240K)] ...13 }14}4. 分配失败场景
场景1:Eden区满
1// Eden: 8MB已用,2MB空闲2// 分配3MB对象3byte[] obj = new byte[3 * 1024 * 1024];45// 流程:6// 1. Eden空间不足7// 2. 触发Young GC8// 3. 清理Eden9// 4. 重新分配场景2:大对象直接进老年代
1// 对象大于Eden区2byte[] bigObj = new byte[20 * 1024 * 1024]; // 20MB34// 流程:5// 1. 对象大于Eden6// 2. 不触发Young GC7// 3. 直接分配到老年代5. 空间分配担保
1// Young GC前检查2if (oldGen.freeSpace < youngGen.usedSpace) {3 // 老年代空间可能不足4 if (HandlePromotionFailure) {5 // 允许冒险,执行Young GC6 youngGC();7 } else {8 // 不允许冒险,执行Full GC9 fullGC();10 }11}6. 相关JVM参数
1# 设置新生代大小2-Xmn512m34# 设置Eden和Survivor比例5-XX:SurvivorRatio=867# 打印GC详情8-XX:+PrintGCDetails910# 打印GC原因11-XX:+PrintGCCause7. 监控Young GC
1# 使用jstat2jstat -gcutil <pid> 100034# 输出5 S0 S1 E O M YGC YGCT6 0.00 50.00 75.00 30.00 95.00 100 0.50078# E: Eden使用率75%9# YGC: Young GC次数100次10# YGCT: Young GC总耗时0.5秒8. 优化建议
1// 1. 合理设置新生代大小2-Xmn1g // 根据应用特点调整34// 2. 避免频繁创建大对象5// 差:6for (int i = 0; i < 1000; i++) {7 byte[] temp = new byte[10 * 1024 * 1024]; // 频繁触发GC8}910// 好:11byte[] buffer = new byte[10 * 1024 * 1024]; // 复用12for (int i = 0; i < 1000; i++) {13 // 使用buffer14}1516// 3. 对象池复用17ObjectPool<Buffer> pool = new ObjectPool<>();18Buffer buffer = pool.borrow();19// 使用20pool.return(buffer);24. 什么情况下会触发 Java 的 Full GC?
答案:
Full GC在多种情况下触发,通常意味着严重的性能问题。
1. 老年代空间不足
1// 场景1:大对象直接进入老年代2byte[] bigObj = new byte[100 * 1024 * 1024]; // 100MB34// 场景2:Young GC后晋升对象过多5if (oldGen.freeSpace < promotionSize) {6 fullGC();7}89// 场景3:长期存活对象累积10public class Service {11 private static List<Object> cache = new ArrayList<>();12 13 public void addCache() {14 cache.add(new Object()); // 持续累积15 // 最终老年代满,触发Full GC16 }17}2. 方法区(元空间)满
1// 动态生成大量类2while (true) {3 Enhancer enhancer = new Enhancer();4 enhancer.setSuperclass(Object.class);5 enhancer.setUseCache(false);6 enhancer.create(); // 生成类7}89// 元空间满,触发Full GC10// java.lang.OutOfMemoryError: Metaspace3. 空间分配担保失败
1// Young GC前检查2long avgPromotionSize = getAveragePromotionSize();34if (oldGen.freeSpace < avgPromotionSize) {5 // 担保失败,触发Full GC6 fullGC();7} else {8 // 执行Young GC9 youngGC();10}1112// 如果Young GC后仍无法晋升13if (promotionFailed) {14 // 再次触发Full GC15 fullGC();16}4. CMS并发失败
1// CMS并发标记期间2// 老年代空间不足以容纳新晋升对象3if (oldGen.freeSpace < promotionSize) {4 // Concurrent Mode Failure5 // 降级为Serial Old,执行Full GC6 fullGC();7}89// 日志:10// [CMS-concurrent-mark: 0.100/0.100 secs]11// [CMS Concurrent Mode Failure]12// [Full GC (Allocation Failure) ...5. 显式调用System.gc()
1// 不推荐2System.gc(); // 建议JVM执行Full GC34// 可以禁用5-XX:+DisableExplicitGC67// 或改为并发GC8-XX:+ExplicitGCInvokesConcurrent6. Dump堆内存
1# jmap触发Full GC2jmap -dump:live,format=b,file=heap.bin <pid>34# live参数会先执行Full GC7. 晋升失败
1// Promotion Failed2// Survivor空间不足,对象直接晋升老年代3// 但老年代也空间不足45// 示例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 GC16 // 对象晋升失败17 // 触发Full GC18 }19 }20}8. Full GC触发条件汇总
| 触发条件 | 说明 | 频率 | 影响 |
|---|---|---|---|
| 老年代满 | 最常见 | 中 | 大 |
| 元空间满 | 类加载过多 | 低 | 大 |
| 担保失败 | 空间预估不足 | 低 | 大 |
| CMS失败 | 并发收集失败 | 低 | 极大 |
| System.gc() | 显式调用 | 取决于代码 | 大 |
| 晋升失败 | 空间碎片 | 低 | 大 |
9. 监控Full GC
1# GC日志2-XX:+PrintGCDetails3-XX:+PrintGCDateStamps4-Xloggc:gc.log56# 日志示例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]1314# jstat监控15jstat -gcutil <pid> 100016 S0 S1 E O M FGC FGCT17 0.00 0.00 10.00 95.00 90.00 10 5.00010. 避免Full GC
1// 1. 合理设置堆大小2-Xms4g -Xmx4g // 初始和最大堆一致34// 2. 调整新生代和老年代比例5-XX:NewRatio=2 // 老年代:新生代 = 2:167// 3. 避免大对象8// 差:9byte[] big = new byte[10 * 1024 * 1024];1011// 好:分批处理12for (int i = 0; i < 10; i++) {13 byte[] small = new byte[1024 * 1024];14 process(small);15}1617// 4. 及时释放引用18list.clear();19cache.evict();2021// 5. 使用对象池22ObjectPool<Buffer> pool = new ObjectPool<>();2324// 6. 选择合适的GC25-XX:+UseG1GC // G1避免Full GC26-XX:MaxGCPauseMillis=20025. 什么是 Java 的 PLAB?
答案:
PLAB(Promotion Local Allocation Buffer)是JVM为每个线程在老年代分配的私有缓冲区,用于优化对象晋升性能。
1. PLAB定义
作用:
- 线程私有的老年代分配缓冲区
- 避免多线程竞争
- 提高对象晋升效率
类比TLAB:
1TLAB: 新生代的线程本地分配缓冲区2PLAB: 老年代的线程本地分配缓冲区2. 为什么需要PLAB
问题:多线程晋升竞争
1// Young GC时,多个线程同时晋升对象2Thread 1: 晋升对象A到老年代3Thread 2: 晋升对象B到老年代4Thread 3: 晋升对象C到老年代56// 没有PLAB:需要同步7synchronized (oldGen) {8 oldGen.allocate(obj); // 性能瓶颈9}解决:使用PLAB
1// 每个线程有自己的PLAB2Thread 1: PLAB1 -> 老年代3Thread 2: PLAB2 -> 老年代4Thread 3: PLAB3 -> 老年代56// 无需同步,并行晋升3. PLAB工作原理
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空间不足,申请新PLAB10 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大小
默认大小:
1# 动态调整2# 根据晋升对象大小自动调整PLAB大小34# 查看PLAB统计5-XX:+PrintPLAB67# 输出示例8PLAB: 1024K, waste: 10K, refills: 5影响因素:
11. 晋升对象的平均大小22. 晋升频率33. 线程数量44. 老年代可用空间5. PLAB vs TLAB对比
| 特性 | TLAB | PLAB |
|---|---|---|
| 位置 | 新生代Eden区 | 老年代 |
| 用途 | 新对象分配 | 对象晋升 |
| 大小 | 较小(几KB) | 较大(几KB到几MB) |
| 频率 | 极高 | 中等 |
| 参数 | -XX:TLABSize | 动态调整 |
6. PLAB优势
性能提升:
1// 无PLAB:2// 每次晋升都需要同步3// 性能:1000次晋升/秒45// 有PLAB:6// 批量晋升,减少同步7// 性能:10000次晋升/秒89// 提升10倍减少碎片:
1PLAB连续分配2→ 减少内存碎片3→ 提高老年代利用率7. 相关参数
1# 打印PLAB信息2-XX:+PrintPLAB34# 最小PLAB大小5-XX:MinPLABSize=102467# 最大PLAB大小8-XX:MaxPLABSize=1048576910# PLAB浪费阈值11-XX:PLABWasteTargetPercent=108. 实际应用
1// Young GC中的PLAB使用2public class YoungGC {3 4 public void evacuate() {5 // 并行GC线程6 parallelDo(() -> {7 // 每个线程有自己的PLAB8 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
1# GC日志中的PLAB信息2-XX:+PrintGCDetails -XX:+PrintPLAB34# 输出示例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]89# waste: PLAB浪费的空间10# refills: PLAB重新分配次数10. 优化建议
1// 1. 减少晋升频率2// 增大新生代,减少Young GC频率3-Xmn2g45// 2. 提高晋升阈值6// 让对象在新生代多停留几次7-XX:MaxTenuringThreshold=1589// 3. 避免大对象晋升10// 大对象直接进老年代,不使用PLAB11-XX:PretenureSizeThreshold=1048576 // 1MB1213// 4. 合理设置老年代大小14// 确保有足够空间分配PLAB15-Xmx4g26. JVM 垃圾回收时产生的 concurrent mode failure 的原因是什么?
答案:
Concurrent Mode Failure是CMS并发收集期间老年代空间不足导致的失败。
主要原因:
- 并发期间晋升过快:Young GC频繁,对象快速晋升到老年代
- 触发时机过晚:CMSInitiatingOccupancyFraction设置过高
- 内存碎片:CMS使用标记-清除,产生碎片导致分配失败
后果:
- 降级为Serial Old单线程Full GC
- 应用暂停时间长(几秒)
- 严重影响性能
解决方案:
1# 提前触发CMS2-XX:CMSInitiatingOccupancyFraction=7034# 增大堆5-Xmx4g67# 或切换到G18-XX:+UseG1GC27. 为什么 Java 中 CMS 垃圾收集器在发生 Concurrent Mode Failure 时的 Full GC 是单线程的?
答案:
CMS失败时降级为Serial Old收集器,这是历史设计决定。
原因:
- 历史设计:CMS设计时选择Serial Old作为后备
- 数据结构不兼容:CMS与Parallel Old数据结构不同
- 实现简单:Serial Old稳定可靠
性能影响:
1正常CMS:暂停50ms2Serial Old Full GC:暂停5000ms(100倍差距)解决方案:
1# 切换到G1(多线程Full GC)2-XX:+UseG1GC28. 为什么 Java 中某些新生代和老年代的垃圾收集器不能组合使用?比如 ParNew 和 Parallel Old
答案:
收集器组合限制源于框架、数据结构和设计目标的不兼容。
可用组合:
1Serial -> Serial Old2ParNew -> CMS3Parallel -> Parallel Old4G1 -> G1不兼容原因:
- 框架不同:ParNew基于CMS框架,Parallel Old基于Parallel框架
- 数据结构不同:Card Table vs Region-based
- 设计目标不同:低延迟 vs 高吞吐量
推荐配置:
1# 低延迟2-XX:+UseG1GC34# 高吞吐5-XX:+UseParallelGC29. JVM 新生代垃圾回收如何避免全堆扫描?
答案:
通过Card Table和写屏障技术避免扫描整个老年代。
Card Table原理:
1// 将老年代划分为512字节的Card2// 用字节数组记录哪些Card有跨代引用3byte[] cardTable;45// 写屏障自动标记6void writeBarrier(Object obj, Object ref) {7 obj.field = ref;8 if (isOld(obj) && isYoung(ref)) {9 cardTable.markDirty(obj);10 }11}Young GC流程:
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 Table | G1 Remembered Set |
|---|---|---|
| 粒度 | 512字节 | Region(1-32MB) |
| 记录内容 | 是否有引用 | 精确引用来源 |
| 空间开销 | 0.2% | 5-10% |
| 扫描效率 | 中等 | 高 |
| 适用场景 | 小堆 | 大堆 |
CMS:
1// 简单标记Card为脏2cardTable[index] = DIRTY;G1:
1// 精确记录引用来源2region.rset.addReference(fromRegion, card);选择建议:
- 小堆(小于4GB):CMS
- 大堆(大于4GB):G1
31. 为什么 G1 垃圾收集器不维护年轻代到老年代的记忆集?
答案:
G1只维护老年代到年轻代的记忆集,因为年轻代总是会被完整收集。
原因分析:
1. 年轻代总是全部收集
1// G1的Young GC2// 总是收集所有年轻代Region3void youngGC() {4 // 收集所有Eden Region5 collectAllEdenRegions();6 7 // 收集所有Survivor Region8 collectAllSurvivorRegions();9 10 // 无需记忆集,因为全部扫描11}2. 老年代部分收集
1// G1的Mixed GC2// 只收集部分老年代Region3void mixedGC() {4 // 收集所有年轻代5 collectYoungRegions();6 7 // 只收集部分老年代8 collectSelectedOldRegions();9 10 // 需要RSet记录其他老年代Region的引用11}3. 记忆集的作用
1记忆集用于:2- 避免扫描整个堆3- 只扫描可能包含引用的区域45年轻代→老年代:6- 不需要RSet7- 因为年轻代总是全部收集8- 所有引用都会被扫描到910老年代→年轻代:11- 需要RSet12- 因为只收集部分老年代13- 需要知道哪些老年代Region引用了年轻代4. 内存开销考虑
1如果维护年轻代→老年代RSet:2- 额外的内存开销3- 额外的维护成本4- 没有实际收益(因为年轻代总是全收集)32. Java 中的 CMS 和 G1 垃圾收集器如何维持并发的正确性?
答案:
CMS使用增量更新,G1使用SATB(原始快照),都通过写屏障维持并发正确性。
CMS增量更新:
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:
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. 可预测的停顿时间
1# G1可以设置停顿时间目标2-XX:MaxGCPauseMillis=20034# CMS无法精确控制停顿时间2. 无内存碎片
1CMS:标记-清除,产生碎片2G1:标记-整理,无碎片3. 大堆支持更好
1CMS:适合小于8GB2G1:适合8-64GB3ZGC:大于64GB4. 分代收集更灵活
1CMS:固定的新生代和老年代2G1:动态调整Region角色5. 避免Concurrent Mode Failure
1CMS:可能降级为Serial Old2G1:使用Evacuation Failure机制,性能影响小6. 整体吞吐量更高
1CMS:并发收集,但碎片影响性能2G1:整理内存,长期性能更稳定34. 什么是 Java 中的 logging write barrier?
答案:
Logging Write Barrier是G1用于维护Remembered Set的写屏障机制。
工作原理:
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}2526// 后台线程处理日志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. 选择所有年轻代Region22. STW,复制存活对象33. 部分对象晋升到老年代44. 时间:10-50ms2. 并发标记周期
1阶段1:初始标记(STW)2- 标记GC Roots3- 时间:几毫秒45阶段2:并发标记6- 并发遍历对象图7- 使用SATB8- 时间:几百毫秒910阶段3:最终标记(STW)11- 处理SATB队列12- 时间:几十毫秒1314阶段4:清理(部分STW)15- 统计Region存活率16- 选择回收集合3. Mixed GC
11. 收集所有年轻代Region22. 收集部分老年代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 Table3- 时间:几十毫秒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停顿时间:小于10ms2与堆大小无关3适用于大堆(大于64GB)2. Colored Pointer
1// 64位指针布局2[unused 16bit][Finalizable 1bit][Remapped 1bit][Marked1 1bit][Marked0 1bit][address 44bit]34// 通过指针颜色记录对象状态5// 无需额外内存开销3. Load Barrier
1// 读屏障,在读取对象时触发2void loadBarrier(Object obj) {3 if (needsBarrier(obj)) {4 // 重定位对象5 obj = relocate(obj);6 }7 return obj;8}4. 并发收集
1所有阶段几乎都是并发的:2- 并发标记3- 并发重定位4- 并发引用处理使用:
1-XX:+UseZGC2-Xmx16g38. JVM 垃圾回收调优的主要目标是什么?
答案:
GC调优目标是平衡延迟、吞吐量和内存占用。
三大目标:
1. 降低延迟
1- 减少GC停顿时间2- 减少GC频率3- 目标:GC停顿小于100ms2. 提高吞吐量
1- 增加应用运行时间占比2- 减少GC时间占比3- 目标:GC时间小于5%3. 控制内存占用
1- 合理设置堆大小2- 避免内存浪费3- 目标:内存利用率>70%不可能三角:
1低延迟 ←→ 高吞吐量2 ↑3 低内存45只能同时满足两个目标39. 如何对 Java 的垃圾回收进行调优?
答案:
GC调优需要分析问题、设置参数、测试验证。
调优步骤:
1. 收集GC日志
1-XX:+PrintGCDetails2-XX:+PrintGCDateStamps3-Xloggc:gc.log2. 分析GC问题
1- Young GC频繁?增大新生代2- Full GC频繁?增大老年代3- GC时间长?切换收集器3. 调整参数
1# 堆大小2-Xms4g -Xmx4g34# 新生代大小5-Xmn1g67# 收集器8-XX:+UseG1GC9-XX:MaxGCPauseMillis=2004. 验证效果
1# 监控指标2jstat -gcutil <pid> 100040. 常用的 JVM 配置参数有哪些?
答案:
内存配置:
1-Xms4g # 初始堆大小2-Xmx4g # 最大堆大小3-Xmn1g # 新生代大小4-Xss1m # 线程栈大小5-XX:MetaspaceSize=256m # 元空间初始大小6-XX:MaxMetaspaceSize=512m # 元空间最大大小收集器选择:
1-XX:+UseG1GC # 使用G12-XX:+UseZGC # 使用ZGC3-XX:+UseConcMarkSweepGC # 使用CMS4-XX:MaxGCPauseMillis=200 # 最大停顿时间GC日志:
1-XX:+PrintGCDetails2-XX:+PrintGCDateStamps3-Xloggc:gc.log4-XX:+UseGCLogFileRotation5-XX:NumberOfGCLogFiles=106-XX:GCLogFileSize=100MOOM处理:
1-XX:+HeapDumpOnOutOfMemoryError2-XX:HeapDumpPath=/logs/heapdump.hprof3-XX:OnOutOfMemoryError="sh /scripts/restart.sh"性能调优:
1-XX:+UseStringDeduplication # 字符串去重2-XX:+UseTLAB # 线程本地分配缓冲41. 你常用哪些工具来分析 JVM 性能?
答案:
命令行工具:
1# jps - 查看Java进程2jps -lvm34# jstat - GC统计5jstat -gcutil <pid> 100067# jmap - 堆转储8jmap -dump:live,format=b,file=heap.bin <pid>910# jstack - 线程堆栈11jstack <pid> > thread.txt1213# 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. 获取堆转储
1# 手动转储2jmap -dump:live,format=b,file=heap.bin <pid>34# 自动转储(OOM时)5-XX:+HeapDumpOnOutOfMemoryError6-XX:HeapDumpPath=/logs/heapdump.hprof2. 使用MAT分析
11. 打开heap.bin22. 查看Dominator Tree33. 找到占用内存最多的对象44. 分析GC Roots引用链55. 定位泄漏代码3. 常见泄漏场景
1// 场景1:静态集合2public class Leak {3 private static List<Object> list = new ArrayList<>();4 public void add() {5 list.add(new Object()); // 永不清理6 }7}89// 场景2:ThreadLocal未清理10ThreadLocal<byte[]> local = new ThreadLocal<>();11local.set(new byte[1024 * 1024]);12// 忘记local.remove()1314// 场景3:监听器未移除15button.addListener(listener);16// 忘记button.removeListener(listener)43. Java 里的对象在虚拟机里面是怎么存储的?
答案:
Java对象在内存中分为对象头、实例数据和对齐填充三部分。
对象结构:
1[对象头][实例数据][对齐填充]1. 对象头(Object Header)
1Mark Word(8字节):2- 哈希码3- GC分代年龄4- 锁状态标志5- 线程ID67Class Pointer(4/8字节):8- 指向类元数据9- 开启指针压缩为4字节1011Array Length(4字节,仅数组):12- 数组长度2. 实例数据
1public class User {2 private int age; // 4字节3 private String name; // 4/8字节(引用)4}3. 对齐填充
1对象大小必须是8字节的倍数2不足部分用填充补齐示例计算:
1public class Point {2 private int x; // 4字节3 private int y; // 4字节4}56// 对象大小: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执行详细流程:
1// 1. 编写代码2public class Hello {3 public static void main(String[] args) {4 System.out.println("Hello");5 }6}78// 2. javac编译9javac Hello.java // 生成Hello.class1011// 3. java运行12java Hello1314// 4. JVM执行15// - 类加载器加载Hello.class16// - 验证字节码合法性17// - 分配内存,初始化静态变量18// - 执行main方法19// - 解释执行或JIT编译20// - 输出Hello学习指南
核心要点:
- JVM 内存模型和垃圾回收机制
- 类加载过程和双亲委派模型
- 性能监控和调优方法
- 常见内存问题排查
学习路径建议:
- 掌握 JVM 内存结构和对象生命周期
- 深入理解垃圾回收算法和收集器
- 学习 JVM 调优参数和监控工具
- 掌握内存泄漏分析和性能优化
评论区 / Comments