【面试特集】JVM 内存与对象
JVM 内存与对象面试题
参见实体页:[[jdk8]]、[[jdk21]],概念页:[[gc-evolution]]
一、运行时数据区
Q1: JVM 内存模型(运行时数据区)?⭐⭐⭐
| 区域 | 线程共享 | 存储内容 | 异常 |
|---|---|---|---|
| 堆(Heap) | 是 | 对象实例、数组 | OutOfMemoryError |
| 方法区(Metaspace) | 是 | 类信息、常量、静态变量、JIT 代码 | OutOfMemoryError |
| 虚拟机栈 | 否 | 栈帧(局部变量表、操作数栈、动态链接、返回地址) | StackOverflowError / OOM |
| 本地方法栈 | 否 | Native 方法的栈帧 | StackOverflowError / OOM |
| 程序计数器 | 否 | 当前执行字节码的行号(Native 方法为 undefined) | 唯一不会 OOM 的区域 |
JDK 8 的变化:
- 永久代(PermGen)→ 元空间(Metaspace)
- Metaspace 使用本地内存(Native Memory),不再受
-Xmx限制 - 字符串常量池从方法区移到堆中
堆内存分代(G1 之前):
Q2: 栈帧的结构?⭐⭐
每个方法调用对应一个栈帧,包含:
局部变量表:
- 存储方法参数和局部变量
- 以 Slot(32位)为单位,long/double 占 2 个 Slot
- 实例方法的 Slot[0] 是
this
操作数栈:
- 字节码指令的操作区域(JVM 是基于栈的虚拟机)
iadd指令:弹出两个 int,压入结果
动态链接:
- 指向运行时常量池中该方法的符号引用
- 支持多态(运行时解析为实际方法)
返回地址:
- 正常返回:调用者的 PC 值
- 异常返回:通过异常表确定
StackOverflowError 原因:
- 递归调用过深,栈帧超过
-Xss(默认 512KB-1MB) - 虚拟线程的栈更小(初始 ~1KB),但可动态扩展
二、对象的内存布局
Q3: 对象在堆中的内存布局?⭐⭐⭐
Mark Word 内容(64位,不同锁状态):
| 锁状态 | 内容 |
|---|---|
| 无锁 | hashCode(31) + 分代年龄(4) + 偏向锁标志(1) + 锁标志(2) |
| 偏向锁 | 线程ID(54) + epoch(2) + 分代年龄(4) + 1 + 01 |
| 轻量级锁 | 指向栈中锁记录的指针(62) + 00 |
| 重量级锁 | 指向Monitor的指针(62) + 10 |
| GC标记 | 空(62) + 11 |
对象大小计算示例:
// 开启压缩指针(-XX:+UseCompressedOops,默认开启)classDemo{inta;// 4 byteslongb;// 8 bytesObjectc;// 4 bytes(压缩指针)}// 对象头:8(Mark Word)+ 4(Klass Pointer)= 12 bytes// 实例数据:4 + 8 + 4 = 16 bytes(字段重排序:long先,避免填充)// 对齐填充:0 bytes(12+16=28,需填充4字节到32)// 总计:32 bytes工具:jol-core(Java Object Layout)可精确查看对象内存布局
System.out.println(ClassLayout.parseInstance(newDemo()).toPrintable());Q4: 对象的创建过程?⭐⭐⭐
TLAB 细节:
- 每个线程在 Eden 区预先申请一块私有缓冲区(默认 Eden 的 1%)
- 线程内分配对象直接在 TLAB 上移动指针,无需同步
- TLAB 满了才需要申请新的 TLAB(此时需要同步)
三、字符串与常量池
Q5: String 的内存模型?intern() 的作用?⭐⭐⭐
字符串常量池(String Pool):
- JDK 7 前:在方法区(PermGen)
- JDK 7+:移到堆中(避免 PermGen OOM)
字面量 vs new:
Strings1="hello";// 常量池中创建/复用Strings2="hello";// 复用常量池中的 "hello"Strings3=newString("hello");// 堆中新建对象,内容指向常量池s1==s2// true(同一常量池引用)s1==s3// false(s3 是堆中新对象)s1==s3.intern()// true(intern() 返回常量池中的引用)intern() 原理(JDK 7+):
- 如果常量池中已有该字符串:返回常量池中的引用
- 如果没有:将堆中该字符串对象的引用放入常量池(不复制对象),返回该引用
String 不可变的原因:
char[]数组被private final修饰(JDK 9+ 改为byte[])- 没有提供修改数组内容的方法
- 好处:线程安全、可缓存 hashCode、可作为 HashMap key
StringBuilder vs StringBuffer:
- StringBuilder:非线程安全,单线程拼接用
- StringBuffer:线程安全(方法加 synchronized),多线程用(实际很少用)
- 循环拼接字符串必须用 StringBuilder,
+在循环内会每次创建新对象
Q6: 为什么 JDK 9 将 String 的 char[] 改为 byte[]?⭐⭐
原因:大多数字符串只包含 Latin-1 字符(ASCII),每个字符只需 1 字节,但 char 占 2 字节,浪费内存。
Compact Strings(JDK 9):
- 新增
coder字段:LATIN1(0)或UTF16(1) - Latin-1 字符串:每个字符 1 字节,内存减半
- 含非 Latin-1 字符:退回 UTF-16(每字符 2 字节)
- 对外 API 不变,性能基本持平
四、OOM 类型与排查
Q7: 各种 OOM 的原因和解决方案?⭐⭐⭐
1. Java heap space
java.lang.OutOfMemoryError: Java heap space- 原因:堆内存不足,对象无法分配
- 排查:
jmap -dump+ MAT 分析,找内存泄漏或大对象 - 解决:增大
-Xmx,或修复内存泄漏
2. GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded- 原因:GC 时间超过 98%,但回收内存不足 2%(连续 5 次)
- 本质:内存泄漏导致堆几乎全是存活对象
- 解决:同上,找内存泄漏
3. Metaspace
java.lang.OutOfMemoryError: Metaspace- 原因:类加载过多(动态代理、CGLIB、Groovy 脚本热加载)
- 排查:
jcmd {pid} VM.class_stats查看类数量 - 解决:
-XX:MaxMetaspaceSize=512m,或排查类加载泄漏
4. Direct buffer memory
java.lang.OutOfMemoryError: Direct buffer memory- 原因:NIO DirectByteBuffer 分配的堆外内存超限
- 解决:
-XX:MaxDirectMemorySize=1g,或排查 NIO 使用
5. Unable to create new native thread
java.lang.OutOfMemoryError: unable to create new native thread- 原因:线程数超过 OS 限制(
/proc/sys/kernel/threads-max) - 解决:减少线程数(用线程池),或
ulimit -u增大限制
6. StackOverflowError
- 原因:递归过深,栈帧超过
-Xss - 解决:增大
-Xss(如-Xss2m),或优化递归为迭代
五、JVM 参数速查
Q8: 常用 JVM 参数?⭐⭐⭐
内存设置:
-Xms2g# 初始堆大小(建议与 Xmx 相同,避免动态扩容)-Xmx2g# 最大堆大小-Xmn512m# Young 区大小(G1 不建议设置)-Xss512k# 每个线程栈大小-XX:MetaspaceSize=256m# Metaspace 初始大小(触发 GC 的阈值)-XX:MaxMetaspaceSize=512m# Metaspace 最大大小-XX:MaxDirectMemorySize=1g# 堆外内存上限GC 选择:
-XX:+UseG1GC# G1(JDK 9+ 默认)-XX:+UseZGC# ZGC(JDK 15+ 生产可用)-XX:+ZGenerational# 分代 ZGC(JDK 21+,推荐)-XX:+UseParallelGC# Parallel GC(高吞吐批处理)诊断:
-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/tmp/heap.hprof -Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m-XX:+PrintCompilation# 打印 JIT 编译信息容器环境:
-XX:+UseContainerSupport# 自动感知容器内存限制(JDK 8u191+,默认开启)-XX:MaxRAMPercentage=75.0# 使用容器内存的 75%(替代固定 -Xmx)-XX:InitialRAMPercentage=50.0# 初始堆占容器内存的 50%六、应用场景总结
| 问题现象 | 排查命令 | 可能原因 |
|---|---|---|
| 堆内存持续增长 | jstat -gcutil {pid} 1000 | 内存泄漏 |
| Full GC 频繁 | jmap -histo {pid} | 老年代对象过多/大对象 |
| Metaspace OOM | jcmd {pid} VM.class_stats | 类加载泄漏 |
| 线程数暴涨 | jstack {pid} | grep "Thread" | wc -l | 线程池配置错误 |
| CPU 飙升 | top -H -p {pid}+jstack | 死循环/频繁 GC |
| 响应延迟高 | jstack {pid} | grep BLOCKED | 锁竞争/死锁 |
