【面试特集】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 之前):

Young Generation(默认占堆 1/3)
Eden(默认 8/10)
Survivor0(默认 1/10)
Survivor1(默认 1/10)
Old Generation(默认占堆 2/3)

Q2: 栈帧的结构?⭐⭐

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

局部变量表:

  • 存储方法参数和局部变量
  • 以 Slot(32位)为单位,long/double 占 2 个 Slot
  • 实例方法的 Slot[0] 是 this

操作数栈:

  • 字节码指令的操作区域(JVM 是基于栈的虚拟机)
  • iadd 指令:弹出两个 int,压入结果

动态链接:

  • 指向运行时常量池中该方法的符号引用
  • 支持多态(运行时解析为实际方法)

返回地址:

  • 正常返回:调用者的 PC 值
  • 异常返回:通过异常表确定

StackOverflowError 原因:

  • 递归调用过深,栈帧超过 -Xss(默认 512KB-1MB)
  • 虚拟线程的栈更小(初始 ~1KB),但可动态扩展

二、对象的内存布局

Q3: 对象在堆中的内存布局?⭐⭐⭐

对象头(Object Header)

├── Mark Word(8 bytes):锁状态、GC年龄、hashCode

└── Klass Pointer(4/8 bytes):指向类元数据(开启压缩指针为4字节)
实例数据(Instance Data)

字段值(父类字段在前)
对齐填充(Padding)

保证对象大小是8字节的倍数

Mark Word 内容(64位,不同锁状态):

锁状态 内容
无锁 hashCode(31) + 分代年龄(4) + 偏向锁标志(1) + 锁标志(2)
偏向锁 线程ID(54) + epoch(2) + 分代年龄(4) + 1 + 01
轻量级锁 指向栈中锁记录的指针(62) + 00
重量级锁 指向Monitor的指针(62) + 10
GC标记 空(62) + 11

对象大小计算示例:

java 复制代码
// 开启压缩指针(-XX:+UseCompressedOops,默认开启)
class Demo {
    int a;      // 4 bytes
    long b;     // 8 bytes
    Object c;   // 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)可精确查看对象内存布局

java 复制代码
System.out.println(ClassLayout.parseInstance(new Demo()).toPrintable());

Q4: 对象的创建过程?⭐⭐⭐

  1. 类加载检查

检查 new 指令的参数是否能在常量池中找到类的符号引用

检查类是否已加载、解析、初始化,否则先执行类加载
2. 分配内存

指针碰撞(Bump the Pointer):堆内存规整时(Serial/ParNew),移动指针

空闲列表(Free List):堆内存不规整时(CMS),维护可用内存列表

线程安全:TLAB(Thread Local Allocation Buffer)优先,无锁分配
3. 初始化零值

将分配的内存空间初始化为零值(int=0, boolean=false, ref=null)

保证字段不赋初值也能使用
4. 设置对象头

填写 Mark Word(hashCode、GC年龄、锁状态)

填写 Klass Pointer(指向类元数据)
5. 执行 方法

按照程序员的意愿初始化(构造方法)

TLAB 细节:

  • 每个线程在 Eden 区预先申请一块私有缓冲区(默认 Eden 的 1%)
  • 线程内分配对象直接在 TLAB 上移动指针,无需同步
  • TLAB 满了才需要申请新的 TLAB(此时需要同步)

三、字符串与常量池

Q5: String 的内存模型?intern() 的作用?⭐⭐⭐

字符串常量池(String Pool):

  • JDK 7 前:在方法区(PermGen)
  • JDK 7+:移到堆中(避免 PermGen OOM)

字面量 vs new:

java 复制代码
String s1 = "hello";           // 常量池中创建/复用
String s2 = "hello";           // 复用常量池中的 "hello"
String s3 = new String("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 参数?⭐⭐⭐

内存设置:

bash 复制代码
-Xms2g                    # 初始堆大小(建议与 Xmx 相同,避免动态扩容)
-Xmx2g                    # 最大堆大小
-Xmn512m                  # Young 区大小(G1 不建议设置)
-Xss512k                  # 每个线程栈大小
-XX:MetaspaceSize=256m    # Metaspace 初始大小(触发 GC 的阈值)
-XX:MaxMetaspaceSize=512m # Metaspace 最大大小
-XX:MaxDirectMemorySize=1g # 堆外内存上限

GC 选择:

bash 复制代码
-XX:+UseG1GC              # G1(JDK 9+ 默认)
-XX:+UseZGC               # ZGC(JDK 15+ 生产可用)
-XX:+ZGenerational        # 分代 ZGC(JDK 21+,推荐)
-XX:+UseParallelGC        # Parallel GC(高吞吐批处理)

诊断:

bash 复制代码
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof
-Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=20m
-XX:+PrintCompilation      # 打印 JIT 编译信息

容器环境:

bash 复制代码
-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"
CPU 飙升 top -H -p {pid} + jstack 死循环/频繁 GC
响应延迟高 `jstack {pid} grep BLOCKED`
相关推荐
2501_901006471 小时前
CSS如何实现多种颜色的线性渐变_使用linear-gradient()按方向和色标填色
jvm·数据库·python
2303_821287381 小时前
Golang怎么用embed嵌入SQL文件_Golang如何将SQL迁移文件嵌入Go程序统一管理【技巧】
jvm·数据库·python
m0_702036531 小时前
PHP怎么处理Eloquent Attribute Harmonization属性协调_Laravel解决数据冲突【教程】
jvm·数据库·python
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第48题】【JVM篇】第8题:JVM 里的有几种 ClassLoader?为什么会有多种?
java·开发语言·jvm·面试
iAm_Ike1 小时前
Redis怎样通过频道划分不同的日志级别
jvm·数据库·python
kexnjdcncnxjs1 小时前
CSS如何利用-nth-of-type(1)修改首个元素样式_通过位置约束精准修饰
jvm·数据库·python
dinglu1030DL1 小时前
Tailwind CSS如何实现鼠标悬停变色_使用hover-bg-blue-500类.txt
jvm·数据库·python
神明9311 小时前
Tailwind CSS如何实现鼠标悬停变色_使用hover-bg-blue-500类
jvm·数据库·python
2401_850491651 小时前
Redis如何监控系统QPS的变化趋势
jvm·数据库·python