少女祈祷中...
NEKO
知识库

java 内存结构面试要点

一、整体结构

Q1:JVM 运行时数据区包含哪些部分?

答: JVM 运行时数据区分为五大区域:

区域

线程私有

作用

程序计数器

✅

当前线程执行字节码的行号

虚拟机栈

✅

方法调用的栈帧

本地方法栈

✅

native 方法调用

堆

❌

对象实例存储

方法区/元空间

❌

类元信息、常量、静态变量

text

JVM 运行时数据区
├── 线程私有
│   ├── 程序计数器(PC Register)
│   ├── 虚拟机栈(VM Stack)
│   └── 本地方法栈(Native Method Stack)
└── 线程共享
    ├── 堆(Heap)
    └── 方法区(Method Area)/ 元空间(Metaspace)

直接内存:不属于运行时数据区,但被 NIO 使用。


Q2:哪些区域是线程私有的?哪些是共享的?

答:

线程私有:

  • 程序计数器

  • 虚拟机栈

  • 本地方法栈

线程共享:

  • 堆

  • 方法区/元空间

原因:

  • 私有区域:每个线程独立执行,需隔离

  • 共享区域:对象和类信息被所有线程使用


Q3:哪些区域会抛 OOM?哪些不会?

答:

区域

异常

程序计数器

❌ 不会 OOM

虚拟机栈

StackOverflowError / OOM

本地方法栈

StackOverflowError / OOM

堆

OutOfMemoryError: Java heap space

方法区/元空间

OutOfMemoryError: Metaspace

直接内存

OutOfMemoryError: Direct buffer memory

程序计数器是唯一不会 OOM 的区域。


二、程序计数器

Q4:程序计数器的作用是什么?

答: 程序计数器(PC Register)记录当前线程正在执行的字节码行号:

  • 字节码解释器通过它读取下一条指令

  • 分支、循环、跳转、异常处理都依赖它

  • 每个线程独立,保证线程切换后能恢复执行位置

java

// 线程 A 执行到第 10 行,被切换
// 线程 B 执行到第 20 行
// 切回 A 时,PC 指向 10,继续执行

Q5:为什么程序计数器是线程私有的?

答: 因为多线程交替执行:

  • 每个线程需要独立记录自己的执行位置

  • 线程切换后,靠 PC 恢复

  • 如果共享,切换后无法恢复

类比:每个线程有自己的"书签"。


Q6:程序计数器会 OOM 吗?

答: 不会。原因:

  • 只存储一个行号(int 或引用)

  • 内存固定,不动态分配

  • JVM 规范未定义 OOM

唯一不会 OOM 的区域。


Q7:程序计数器的值可以为 null 吗?

答: 可以。执行 native 方法时:

  • native 方法由 C/C++ 执行

  • 不走字节码

  • PC 值为 undefined(null)

java

public native void nativeMethod(); // PC = null

三、虚拟机栈

Q8:虚拟机栈的作用是什么?

答: 虚拟机栈(VM Stack)存储方法调用的栈帧:

  • 每个方法调用创建一个栈帧

  • 栈帧包含:局部变量表、操作数栈、动态链接、返回地址

  • 方法执行完毕,栈帧出栈

java

public void methodA() {
    int x = 1;      // 局部变量
    methodB();      // 调用 methodB,创建新栈帧
}

生命周期:与线程相同。


Q9:栈帧的结构是什么?

答: 栈帧包含四部分:

部分

作用

局部变量表

存储方法参数和局部变量

操作数栈

字节码执行的工作区

动态链接

指向运行时常量池的方法引用

返回地址

方法返回后继续执行的位置

text

栈帧
├── 局部变量表(Local Variables)
├── 操作数栈(Operand Stack)
├── 动态链接(Dynamic Linking)
└── 返回地址(Return Address)

Q10:局部变量表存什么?

答:

  • 方法参数

  • 局部变量(基本类型、对象引用)

  • 编译期确定大小

java

public void method(int a, String b) {
    int x = 1;
    Object obj = new Object();
}
// 局部变量表:
// slot 0: this
// slot 1: a
// slot 2: b
// slot 3: x
// slot 4: obj

注意:

  • 实例方法 slot 0 是 this

  • long/double 占 2 个 slot

  • 引用类型存的是引用,不是对象本身


Q11:操作数栈的作用是什么?

答: 字节码执行的工作区:

java

int a = 1;
int b = 2;
int c = a + b;

字节码:

text

iconst_1        // 压入 1
istore_1        // 弹出到 a
iconst_2        // 压入 2
istore_2        // 弹出到 b
iload_1         // 压入 a
iload_2         // 压入 b
iadd            // 弹出两个,相加,压回结果
istore_3        // 弹出到 c

特点:后进先出(LIFO)。


Q12:什么情况下会抛 StackOverflowError?

答: 栈深度超过限制:

java

public void recursive() {
    recursive(); // 无限递归
}
// 抛 StackOverflowError

原因:

  • 每个方法调用创建一个栈帧

  • 栈帧太多,超过栈大小

  • 默认栈大小:512KB ~ 1MB(-Xss 设置)

解决:

  • 优化递归为循环

  • 增大 -Xss


Q13:什么情况下会抛 OOM?

答: 栈动态扩展时无法申请内存:

  • 栈允许动态扩展(HotSpot 不支持)

  • 扩展时内存不足,抛 OOM

  • 实际少见,多数是 StackOverflowError

bash

-Xss1m  # 设置栈大小

Q14:-Xss 参数的作用是什么?

答: 设置每个线程的栈大小:

bash

-Xss512k   # 512KB
-Xss1m     # 1MB(默认)
-Xss2m     # 2MB

影响:

  • 栈越大,可递归深度越深

  • 栈越大,可创建线程数越少

  • 总内存 = 线程数 × 栈大小

注意:JDK 5 后默认 1MB,之前 256KB。


四、本地方法栈

Q15:本地方法栈的作用是什么?

答: 为 native 方法服务:

java

public native int hashCode();
public native void notify();
  • 与虚拟机栈类似

  • HotSpot 中与虚拟机栈合并

  • 存储 native 方法的栈帧


Q16:本地方法栈和虚拟机栈有什么区别?

答:

对比项

虚拟机栈

本地方法栈

服务对象

Java 方法

native 方法

语言

Java 字节码

C/C++

HotSpot

合并实现

合并实现

HotSpot 中两者是同一块内存,其他 JVM 可能分开。


五、堆

Q17:堆的作用是什么?

答: 堆(Heap)是对象实例的存储区域:

  • 所有 new 的对象都在堆

  • 线程共享

  • GC 的主要区域

java

Object obj = new Object(); // obj 在堆

特点:

  • 最大的一块内存

  • 唯一目的:存放对象实例

  • 可动态扩展


Q18:堆的分代结构是什么?

答: 堆分为新生代和老年代:

text

堆
├── 新生代(Young,1/3)
│   ├── Eden(8/10)
│   ├── Survivor0(1/10)
│   └── Survivor1(1/10)
└── 老年代(Old,2/3)

默认比例:

  • 新生代 : 老年代 = 1 : 2(-XX:NewRatio=2)

  • Eden : S0 : S1 = 8 : 1 : 1(-XX:SurvivorRatio=8)


Q19:对象在堆中的分配过程是什么?

答:

text

1. new 对象
   ↓
2. 栈上分配?(逃逸分析)→ 是 → 栈上分配
   ↓ 否
3. TLAB 分配?(线程本地缓冲)→ 是 → TLAB 分配
   ↓ 否
4. Eden 分配
   ↓
5. Eden 满 → Minor GC → 存活对象进 S0
   ↓
6. 再次 GC → S0 存活进 S1,年龄 +1
   ↓
7. 年龄达 15(-XX:MaxTenuringThreshold)→ 老年代

大对象:直接进老年代(-XX:PretenureSizeThreshold)。


Q20:什么是 TLAB?

答: TLAB(Thread Local Allocation Buffer)是线程私有的分配缓冲区:

  • 每个线程在 Eden 中有一块私有区域

  • 分配对象时优先在 TLAB

  • 避免多线程竞争

bash

-XX:+UseTLAB           # 开启(默认)
-XX:TLABSize=256k      # 大小
-XX:-ResizeTLAB        # 禁止动态调整

流程:

  1. 对象在 TLAB 分配

  2. TLAB 用完,申请新 TLAB

  3. 大对象直接在 Eden 分配


Q21:什么情况下会抛堆 OOM?

答:

java

// 1. 内存泄漏
List<Object> list = new ArrayList<>();
while (true) {
    list.add(new Object()); // 持续增长
}

// 2. 大对象
byte[] big = new byte[Integer.MAX_VALUE];

// 3. 超过最大堆

异常信息:

text

java.lang.OutOfMemoryError: Java heap space

参数:

bash

-Xms512m   # 初始堆
-Xmx2g     # 最大堆
-XX:+HeapDumpOnOutOfMemoryError  # OOM 时 dump

Q22:堆是线程共享的吗?如何保证线程安全?

答: 是线程共享的。

线程安全:

  • 堆本身不保证线程安全

  • 需要开发者用 synchronized、Lock、原子类

  • TLAB 是线程私有的,避免分配竞争

java

// 堆共享,需同步
synchronized (obj) {
    obj.count++;
}

Q23:逃逸分析和栈上分配是什么?

答:

  • 逃逸分析:判断对象是否逃出方法

  • 栈上分配:未逃逸对象直接在栈分配,随栈销毁

java

public void method() {
    Object obj = new Object(); // 未逃逸
    // 可能栈上分配
}

public Object method2() {
    return new Object(); // 逃逸
    // 堆上分配
}

参数:

bash

-XX:+DoEscapeAnalysis  # 开启(默认)
-XX:+EliminateAllocations  # 标量替换

好处:减少 GC 压力。


六、方法区 / 元空间

Q24:方法区的作用是什么?

答: 方法区(Method Area)存储类元信息:

  • 类结构(字段、方法、接口)

  • 运行时常量池

  • 静态变量

  • JIT 编译后的代码

java

public class User {
    static int count = 0; // 静态变量
    public void method() {} // 方法信息
}

线程共享。


Q25:方法区和永久代、元空间有什么关系?

答: 历史演进:

版本

实现

JDK 7 及之前

永久代(PermGen)

JDK 8+

元空间(Metaspace)

永久代:

  • 在 JVM 堆内

  • 大小固定(-XX:MaxPermSize)

  • 易 OOM

元空间:

  • 在本地内存(堆外)

  • 默认无上限

  • 受物理内存限制

bash

# JDK 7
-XX:PermSize=128m -XX:MaxPermSize=256m

# JDK 8+
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m

Q26:为什么 JDK 8 用元空间替代永久代?

答:

  1. 永久代大小难调:太小 OOM,太大浪费

  2. GC 效率低:永久代 GC 复杂

  3. 融合 HotSpot 和 JRockit:JRockit 无永久代

  4. 字符串常量池移出:JDK 7 移到堆

好处:

  • 元空间用本地内存,不易 OOM

  • 简化 GC

  • 更好的兼容性


Q27:元空间会 OOM 吗?

答: 会。虽然默认无上限,但:

  • 受物理内存限制

  • 可设置 -XX:MaxMetaspaceSize

  • 类加载过多会 OOM

java

// 动态生成类,导致元空间 OOM
while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Object.class);
    enhancer.create();
}

异常:

text

java.lang.OutOfMemoryError: Metaspace

Q28:运行时常量池是什么?

答: 方法区的一部分,存储编译期常量和符号引用:

java

public class Test {
    // 字面量
    private static final String S = "hello";
    private static final int N = 100;
    
    // 符号引用
    // 方法名、字段名、类名
}

JDK 7 后:字符串常量池移到堆。

JDK 8 后:运行时常量池在元空间。


Q29:字符串常量池在哪个区域?

答: JDK 7 后在堆:

版本

位置

JDK 6 及之前

永久代

JDK 7+

堆

JDK 8+

堆

原因:永久代 GC 效率低,字符串常量频繁创建。

java

String s1 = "hello";        // 常量池
String s2 = new String("hello"); // 堆
System.out.println(s1 == s2);     // false
System.out.println(s1 == s2.intern()); // true

Q30:方法区存的是对象还是引用?

答: 存类元信息,不是对象:

  • 类的结构信息(字段、方法)

  • 静态变量的引用(JDK 7 后静态变量本身在堆)

  • 常量池

java

public class User {
    static User instance = new User();
    // instance 引用在方法区
    // new User() 对象在堆
}

JDK 7 后:静态变量移到堆。


七、直接内存

Q31:什么是直接内存?

答: 直接内存(Direct Memory)是堆外的本地内存:

  • 不是 JVM 运行时数据区

  • 通过 Unsafe.allocateMemory() 分配

  • NIO 的 DirectByteBuffer 使用

java

ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

特点:

  • 不受堆大小限制

  • 受物理内存限制

  • 分配/释放开销大


Q32:直接内存和堆内存有什么区别?

答:

对比项

堆内存

直接内存

位置

JVM 堆

本地内存

管理

GC

手动/Cleaner

分配速度

快

慢

IO 性能

需拷贝

零拷贝

大小限制

-Xmx

物理内存

OOM

Java heap space

Direct buffer memory

java

// 堆内存
ByteBuffer heap = ByteBuffer.allocate(1024);

// 直接内存
ByteBuffer direct = ByteBuffer.allocateDirect(1024);

Q33:为什么 NIO 用直接内存?

答: 因为零拷贝:

  • 堆内存 IO:需拷贝到堆外再发送

  • 直接内存 IO:直接发送,无需拷贝

text

堆内存:堆 → 堆外 → 网卡(2 次拷贝)
直接内存:直接内存 → 网卡(1 次拷贝)

代价:

  • 分配慢

  • 需手动释放


Q34:直接内存会 OOM 吗?

答: 会:

java

List<ByteBuffer> list = new ArrayList<>();
while (true) {
    list.add(ByteBuffer.allocateDirect(1024 * 1024));
}

异常:

text

java.lang.OutOfMemoryError: Direct buffer memory

参数:

bash

-XX:MaxDirectMemorySize=256m  # 默认与 -Xmx 相同

Q35:直接内存如何释放?

答: 通过 Cleaner 机制:

  • DirectByteBuffer 内部有 Cleaner

  • GC 回收 DirectByteBuffer 对象时

  • Cleaner 调用 Unsafe.freeMemory() 释放

java

// 手动释放(不推荐)
((DirectBuffer) buffer).cleaner().clean();

// 推荐:让 GC 自动回收
buffer = null;
System.gc(); // 触发 GC

注意:直接内存不受 GC 直接管理,可能延迟释放。


八、综合与对比

Q36:堆和栈有什么区别?

答:

对比项

堆

栈

存储

对象实例

局部变量、栈帧

线程

共享

私有

大小

大

小

分配

动态

编译期确定

回收

GC

自动出栈

异常

OOM

StackOverflowError

性能

慢

快

java

Object obj = new Object();
// obj 引用在栈,对象在堆

Q37:栈上分配和堆上分配有什么区别?

答:

对比项

栈上分配

堆上分配

条件

未逃逸

默认

回收

随栈销毁

GC

性能

快

慢

线程

私有

共享

栈上分配依赖逃逸分析,是 JIT 优化。


Q38:为什么方法区叫"非堆"?

答: 因为逻辑上属于堆,物理上独立:

  • JVM 规范把方法区描述为堆的逻辑部分

  • 但 HotSpot 中永久代在堆内,元空间在堆外

  • 习惯上称为"非堆"

JDK 8 后:元空间在本地内存,与堆完全分离。


Q39:如何查看 JVM 内存使用情况?

答:

bash

# jps:查看 Java 进程
jps -l

# jstat:查看 GC 统计
jstat -gc <pid> 1000

# jmap:查看堆内存
jmap -heap <pid>
jmap -histo <pid>

# jstack:查看线程栈
jstack <pid>

# jcmd:综合工具
jcmd <pid> VM.memory

Arthas:

bash

memory      # 内存使用
heapdump    # dump 堆

Q40:如何设置各区域大小?

答:

bash

# 堆
-Xms2g                    # 初始堆
-Xmx4g                    # 最大堆
-Xmn1g                    # 新生代
-XX:NewRatio=2            # 老年代/新生代
-XX:SurvivorRatio=8       # Eden/Survivor

# 栈
-Xss1m                    # 每线程栈

# 元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=512m

# 直接内存
-XX:MaxDirectMemorySize=256m

# OOM 时 dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path

Q41:OOM 有哪些类型?如何排查?

答:

类型

原因

排查

Java heap space

堆对象过多

jmap -histo

Metaspace

类加载过多

jstat -gc

Direct buffer memory

直接内存泄漏

NMT

Unable to create new native thread

线程过多

jstack

GC overhead limit exceeded

GC 频繁

GC 日志

排查步骤:

  1. jps 找进程

  2. jstat -gc 看 GC

  3. jmap -dump 拿堆快照

  4. MAT/Arthas 分析


Q42:堆外内存有哪些?

答:

  1. 直接内存:NIO 的 DirectByteBuffer

  2. 元空间:JDK 8+ 类元信息

  3. JVM 自身:JIT 代码、GC 数据结构

  4. 线程栈:每个线程的栈

text

总内存 = 堆 + 元空间 + 直接内存 + 线程栈 + JVM 自身

参数:

bash

-XX:MaxDirectMemorySize  # 直接内存
-XX:MaxMetaspaceSize     # 元空间
-Xss                     # 线程栈

Q43:为什么 JVM 要分代?

答: 基于对象生命周期的统计:

  • 大多数对象朝生夕死

  • 少数对象长期存活

  • 分代后可用不同 GC 策略

好处:

  • 新生代:复制算法,高效

  • 老年代:标记清除/整理

  • 减少 GC 停顿


Q44:对象在内存中的布局是什么?

答: 三部分:

部分

内容

对象头

Mark Word + 类型指针

实例数据

字段值

对齐填充

8 字节对齐

text

对象
├── Mark Word(8 字节):hash、GC 年龄、锁状态
├── 类型指针(4/8 字节):指向类元数据
├── 实例数据:字段
└── 对齐填充:补齐 8 的倍数

Q45:如何判断对象是否存活?

答:

  1. 引用计数法:有循环引用问题,JVM 不用

  2. 可达性分析:从 GC Roots 出发,不可达则回收

GC Roots:

  • 虚拟机栈中的引用

  • 方法区静态变量

  • 方法区常量

  • 本地方法栈 JNI 引用

评论