JVM面试题
JVM 面试题
Java 内存模型中堆(Heap)和栈(Stack)的区别?
对比维度 | 堆(Heap) | 栈(Stack,通常指虚拟机栈) |
---|---|---|
存储内容 | 所有对象实例(new 创建的对象)、数组 |
局部变量(方法内定义的变量)、方法调用信息(栈帧,包含操作数栈、局部变量表等) |
线程共享性 | 所有线程共享,是线程不安全的(需同步机制保障) | 线程私有,每个线程有独立的栈,线程间不共享 |
生命周期 | 与 JVM 进程一致(随 JVM 启动而创建,退出而销毁) | 与线程 / 方法调用绑定:线程启动时创建栈,方法调用时创建栈帧,方法结束时栈帧销毁 |
内存管理 | 由 JVM 自动管理,依赖垃圾回收器(GC)回收内存 | 无需 GC,随方法调用 / 线程结束自动释放内存(栈帧出栈) |
大小与调整 | 内存空间较大(通常几 GB),可通过-Xms (初始)、-Xmx (最大)参数调整 |
内存空间较小(通常几 MB),可通过-Xss 参数调整单个线程的栈大小 |
异常类型 | 内存不足时触发OutOfMemoryError: Java heap space |
栈深度超限触发StackOverflowError ;栈扩展失败触发OutOfMemoryError: Stack overflow (部分 JVM) |
什么情况下会触发OutOfMemoryError?
OutOfMemoryError
本质是 “内存区域无法分配新空间”,不同内存区域的 OOM 触发原因不同,具体如下:
1. 堆内存溢出(Java heap space
)
触发原因:堆中创建的对象 / 数组过多,且无法被垃圾回收(存在有效引用),导致堆空间耗尽。
典型场景:
内存泄漏:对象不再使用但仍被引用(如静态集合持有大量过期对象),例如:
1
2
3
4static List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object()); // 对象被静态集合引用,无法GC,最终堆溢出
}对象创建速度超过 GC 回收速度:短时间内创建大量对象(如高并发下的大对象频繁实例化),堆空间无法及时释放。
解决思路:通过
-Xmx
调大堆内存;排查内存泄漏(用 MAT 等工具分析堆快照);优化对象创建逻辑(如复用对象、使用池化技术)
2. 方法区 / 元空间溢出(Metaspace OOM
或 PermGen space
)
触发原因:方法区(JDK8 + 为元空间,JDK7 及之前为永久代)存储类信息、常量、静态变量等,当加载的类过多或常量池过大时,空间耗尽。
典型场景:
动态生成大量类:如使用 CGLib、JDK 动态代理等频繁生成子类(每个类的信息都存于方法区),例如:
1
2
3
4
5while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.create(); // 每次创建都会生成新的代理类,类信息累积导致元空间溢出
}常量池过大:字符串常量池(JDK7 后移至堆,但部分实现仍可能关联方法区)存储过多字符串,且无法被回收(如
String.intern()
滥用)。
3. 虚拟机栈 / 本地方法栈溢出(StackOverflowError
与 Stack OOM
)
StackOverflowError
(更常见):线程栈深度超过最大限制(方法调用层级过深,如无限递归),例如:
1 | public void recursive() { |
OutOfMemoryError: Stack overflow
(部分 JVM):如果 JVM 允许栈动态扩展,当扩展时无法申请到足够内存(如创建大量线程,每个线程栈占用内存累积超过系统限制),例如:
1 | while (true) { |
- 解决思路:通过
-Xss
调大单个线程栈大小(但会减少最大线程数);避免无限递归;控制线程创建数量(用线程池)。
4. 直接内存溢出(Direct buffer memory
)
触发原因:直接内存(不受 JVM 堆管理,由操作系统直接分配)通常用于 NIO(
DirectByteBuffer
),当分配的直接内存超过限制(默认与堆最大值一致),会触发 OOM。典型场景:
1
2
3while (true) {
ByteBuffer.allocateDirect(1024 * 1024); // 持续分配直接内存,超过限制后溢出
}解决思路:通过
-XX:MaxDirectMemorySize
调大直接内存限制;及时释放不再使用的DirectByteBuffer
(避免长期引用)。