1. JVM 运行时数据区有哪些?各区域的作用和 OOM 风险?

五大区域(按线程归属分):

🎯 面试要点

  • 栈管运行(方法调用),堆管存储(对象实例)——背下这句,好理解
  • JDK8 为什么去永久代:永久代有大小上限且与堆连在一起,元空间用本地内存,且字符串常量池和静态变量在 JDK7 已移到堆中
  • 字符串常量池在堆中(JDK7+),运行时常量池在方法区/元空间
  • OOM 常见类型:Java heap space(堆)、GC overhead limit exceeded(GC 效率极低)、Metaspace、unable to create new native thread(创建线程失败,可能是栈内存不足)

2. new 一个对象,JVM 内部发生了什么?

  1. 类加载检查:检查常量池能否定位到类符号引用,类是否已加载/解析/初始化(未加载则先加载)
  2. 分配内存:
    • 指针碰撞(Bump the Pointer):堆内存规整(Serial/ParNew 用复制整理)→ 移动指针即可
    • 空闲列表(Free List):堆内存碎片化(CMS 用标记清除)→ 维护可用块列表
    • 并发安全:CAS + 失败重试,或 TLAB(线程本地分配缓冲区,Eden 中每线程预分配一块,优先在 TLAB 分配,避免锁竞争)
  3. 初始化零值:实例字段赋默认值(int=0、引用=null),保证不赋初值也能用
  4. 设置对象头:Mark Word(hashCode、GC 分代年龄、锁状态标志)、类型指针(指向类元数据)
  5. 执行构造方法:先执行父类构造(super()),再执行实例代码块和本类构造,按字节码顺序把字段初始化

🎯 面试要点

  • 对象分配位置有优先级:栈上分配(逃逸分析)→ TLAB → Eden → 大对象直接进老年代
  • 大对象阈值 -XX:PretenureSizeThreshold,避免大对象在新生代来回拷贝
  • 对象进入老年代:年龄计数器达到 MaxTenuringThreshold(默认 15)或动态年龄判定

3. 对象在内存中的布局(HotSpot)

估算对象大小
// 64 位 JVM,开启压缩指针(默认)
// 普通对象:对象头 12B(Mark Word 8B + 类型指针 4B)+ 字段 + 对齐
class User {
    int age;        // 4B
    String name;   // 4B(压缩引用)
}
// 12 + 4 + 4 = 20 → 对齐到 24 字节

// 数组:对象头 16B(多 4B 长度)+ 元素 × 大小,同样 8 字节对齐
int[] arr = new int[10];   // 16 + 40 = 56 → 对齐 56(已是 8 的倍数)

🎯 面试要点

  • 压缩指针 -XX:+UseCompressedOops:堆 ≤ 32G 时引用 8B → 4B,省内存
  • 字段重排:HotSpot 会按宽度重排字段减少填充(boolean/byte 靠后)
  • Mark Word 里存了锁信息,这是 synchronized 锁升级的硬件基础(见并发模块)

4. 什么是逃逸分析?JIT 会做什么优化?

逃逸分析:JIT 编译器分析对象作用域——对象只在方法内部使用(不逃逸)还是被方法外引用(逃逸)?基于分析结果做三个优化:

锁消除示例
public static String concat(String a, String b) {
    StringBuffer sb = new StringBuffer();  // 局部对象,不可能被其他线程访问
    sb.append(a).append(b);                  // JIT 消除 synchronized 开销
    return sb.toString();
}

🎯 面试要点

  • 逃逸分析开关:-XX:+DoEscapeAnalysis(JDK 默认开启)
  • 栈上分配依赖标量替换:-XX:+EliminateAllocations
  • 并非所有对象都能栈上分配:大对象、逃逸对象不行

5. 什么是直接内存(Direct Memory)?

🎯 面试要点

  • 零拷贝的两种理解:堆外直接内存 + sendfile(见网络模块)
  • NIO 的 ByteBuffer.allocateDirect() 分配直接内存
  • 直接内存 OOM 时 top 命令看 RSS 内存会很高,但堆没满——注意区分

🎤 常见面试追问

  1. JDK8 为什么用元空间替代永久代?——永久代有大小上限且与堆耦合,容易 OOM(字符串/类元数据多时);元空间用本地内存,且字符串常量池和静态变量已移入堆。
  2. 对象一定在堆上分配吗?——不一定:逃逸分析后的小对象可能栈上分配/标量替换(JIT 优化),但语义上仍按堆对象理解。
  3. 怎么确认对象在哪个区域?——绝大多数 new 对象在 Eden;大对象(超 PretenureSizeThreshold)直接进老年代;存活 15 次(可调)进老年代。
  4. 堆内存 OOM 和栈溢出怎么区分?——OutOfMemoryError: Java heap space(堆满,对象太多/泄漏);StackOverflowError(栈深超限,递归太深)。报错信息直接区分。
  5. 直接内存为什么不受 GC 管理?——它分配在堆外(Unsafe 分配),JVM 无法按对象回收,靠 Cleaner(虚引用)机制清理,用不好就 Direct buffer memory OOM。

📖 名词解释(本页术语)

术语 大白话解释
堆(Heap)JVM 最大的内存区,所有对象住这里,GC 的主战场。分新生代(Eden+Survivor)和老年代。
虚拟机栈(Java 栈)每个线程一块,方法调用压栈帧(局部变量/操作数栈),方法返回弹栈。递归太深会 StackOverflowError。
方法区 / 元空间存类的元信息(类名/方法描述)、运行时常量池、静态变量。JDK8 起叫元空间,用本地内存。
程序计数器(PC)记录当前线程执行到字节码哪一行,线程私有,唯一不会 OOM 的区域。
对象头(Mark Word)对象开头的一段信息:哈希、GC 年龄、锁状态(synchronized 锁升级全靠它)。
TLAB线程本地分配缓冲区:Eden 里给每个线程预分一小块,分配对象不用抢锁,快。
逃逸分析JIT 分析对象是否逃出方法:不逃逸 → 栈上分配/标量替换/锁消除,减少 GC 压力。
直接内存(Direct Memory)堆外内存(NIO 的 DirectByteBuffer),省一次堆内外拷贝,Netty 零拷贝的基础。不归 GC 管,需手动回收。
OOM / StackOverflowOutOfMemoryError:内存耗尽(堆/元空间/直接内存);StackOverflowError:栈溢出(递归太深)。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。