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
      4
      static List<Object> list = new ArrayList<>();
      while (true) {
      list.add(new Object()); // 对象被静态集合引用,无法GC,最终堆溢出
      }
    • 对象创建速度超过 GC 回收速度:短时间内创建大量对象(如高并发下的大对象频繁实例化),堆空间无法及时释放。

  • 解决思路:通过-Xmx调大堆内存;排查内存泄漏(用 MAT 等工具分析堆快照);优化对象创建逻辑(如复用对象、使用池化技术)

2. 方法区 / 元空间溢出(Metaspace OOMPermGen space

  • 触发原因:方法区(JDK8 + 为元空间,JDK7 及之前为永久代)存储类信息、常量、静态变量等,当加载的类过多或常量池过大时,空间耗尽。

  • 典型场景

    • 动态生成大量类:如使用 CGLib、JDK 动态代理等频繁生成子类(每个类的信息都存于方法区),例如:

      1
      2
      3
      4
      5
      while (true) {
      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(User.class);
      enhancer.create(); // 每次创建都会生成新的代理类,类信息累积导致元空间溢出
      }
    • 常量池过大:字符串常量池(JDK7 后移至堆,但部分实现仍可能关联方法区)存储过多字符串,且无法被回收(如String.intern()滥用)。

3. 虚拟机栈 / 本地方法栈溢出(StackOverflowErrorStack OOM

StackOverflowError(更常见):线程栈深度超过最大限制(方法调用层级过深,如无限递归),例如:

1
2
3
public void recursive() {
recursive(); // 无限递归,栈帧不断入栈,最终栈深度超限
}

OutOfMemoryError: Stack overflow(部分 JVM):如果 JVM 允许栈动态扩展,当扩展时无法申请到足够内存(如创建大量线程,每个线程栈占用内存累积超过系统限制),例如:

1
2
3
4
5
while (true) {
new Thread(() -> {
while (true) {} // 线程不退出,持续占用栈内存,最终总栈内存耗尽
}).start();
}
  • 解决思路:通过-Xss调大单个线程栈大小(但会减少最大线程数);避免无限递归;控制线程创建数量(用线程池)。

4. 直接内存溢出(Direct buffer memory

  • 触发原因:直接内存(不受 JVM 堆管理,由操作系统直接分配)通常用于 NIO(DirectByteBuffer),当分配的直接内存超过限制(默认与堆最大值一致),会触发 OOM。

  • 典型场景

    1
    2
    3
    while (true) {
    ByteBuffer.allocateDirect(1024 * 1024); // 持续分配直接内存,超过限制后溢出
    }
  • 解决思路:通过-XX:MaxDirectMemorySize调大直接内存限制;及时释放不再使用的DirectByteBuffer(避免长期引用)。