一、整体结构
Q1:JVM 运行时数据区包含哪些部分?
答: JVM 运行时数据区分为五大区域:
text
JVM 运行时数据区
├── 线程私有
│ ├── 程序计数器(PC Register)
│ ├── 虚拟机栈(VM Stack)
│ └── 本地方法栈(Native Method Stack)
└── 线程共享
├── 堆(Heap)
└── 方法区(Method Area)/ 元空间(Metaspace)直接内存:不属于运行时数据区,但被 NIO 使用。
Q2:哪些区域是线程私有的?哪些是共享的?
答:
线程私有:
程序计数器
虚拟机栈
本地方法栈
线程共享:
堆
方法区/元空间
原因:
私有区域:每个线程独立执行,需隔离
共享区域:对象和类信息被所有线程使用
Q3:哪些区域会抛 OOM?哪些不会?
答:
程序计数器是唯一不会 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:本地方法栈和虚拟机栈有什么区别?
答:
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 # 禁止动态调整流程:
对象在 TLAB 分配
TLAB 用完,申请新 TLAB
大对象直接在 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 时 dumpQ22:堆是线程共享的吗?如何保证线程安全?
答: 是线程共享的。
线程安全:
堆本身不保证线程安全
需要开发者用 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:方法区和永久代、元空间有什么关系?
答: 历史演进:
永久代:
在 JVM 堆内
大小固定(
-XX:MaxPermSize)易 OOM
元空间:
在本地内存(堆外)
默认无上限
受物理内存限制
bash
# JDK 7
-XX:PermSize=128m -XX:MaxPermSize=256m
# JDK 8+
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512mQ26:为什么 JDK 8 用元空间替代永久代?
答:
永久代大小难调:太小 OOM,太大浪费
GC 效率低:永久代 GC 复杂
融合 HotSpot 和 JRockit:JRockit 无永久代
字符串常量池移出: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: MetaspaceQ28:运行时常量池是什么?
答: 方法区的一部分,存储编译期常量和符号引用:
java
public class Test {
// 字面量
private static final String S = "hello";
private static final int N = 100;
// 符号引用
// 方法名、字段名、类名
}JDK 7 后:字符串常量池移到堆。
JDK 8 后:运行时常量池在元空间。
Q29:字符串常量池在哪个区域?
答: JDK 7 后在堆:
原因:永久代 GC 效率低,字符串常量频繁创建。
java
String s1 = "hello"; // 常量池
String s2 = new String("hello"); // 堆
System.out.println(s1 == s2); // false
System.out.println(s1 == s2.intern()); // trueQ30:方法区存的是对象还是引用?
答: 存类元信息,不是对象:
类的结构信息(字段、方法)
静态变量的引用(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:直接内存和堆内存有什么区别?
答:
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内部有CleanerGC 回收
DirectByteBuffer对象时Cleaner调用Unsafe.freeMemory()释放
java
// 手动释放(不推荐)
((DirectBuffer) buffer).cleaner().clean();
// 推荐:让 GC 自动回收
buffer = null;
System.gc(); // 触发 GC注意:直接内存不受 GC 直接管理,可能延迟释放。
八、综合与对比
Q36:堆和栈有什么区别?
答:
java
Object obj = new Object();
// obj 引用在栈,对象在堆Q37:栈上分配和堆上分配有什么区别?
答:
栈上分配依赖逃逸分析,是 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.memoryArthas:
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=/pathQ41:OOM 有哪些类型?如何排查?
答:
排查步骤:
jps找进程jstat -gc看 GCjmap -dump拿堆快照MAT/Arthas 分析
Q42:堆外内存有哪些?
答:
直接内存:NIO 的 DirectByteBuffer
元空间:JDK 8+ 类元信息
JVM 自身:JIT 代码、GC 数据结构
线程栈:每个线程的栈
text
总内存 = 堆 + 元空间 + 直接内存 + 线程栈 + JVM 自身参数:
bash
-XX:MaxDirectMemorySize # 直接内存
-XX:MaxMetaspaceSize # 元空间
-Xss # 线程栈Q43:为什么 JVM 要分代?
答: 基于对象生命周期的统计:
大多数对象朝生夕死
少数对象长期存活
分代后可用不同 GC 策略
好处:
新生代:复制算法,高效
老年代:标记清除/整理
减少 GC 停顿
Q44:对象在内存中的布局是什么?
答: 三部分:
text
对象
├── Mark Word(8 字节):hash、GC 年龄、锁状态
├── 类型指针(4/8 字节):指向类元数据
├── 实例数据:字段
└── 对齐填充:补齐 8 的倍数Q45:如何判断对象是否存活?
答:
引用计数法:有循环引用问题,JVM 不用
可达性分析:从 GC Roots 出发,不可达则回收
GC Roots:
虚拟机栈中的引用
方法区静态变量
方法区常量
本地方法栈 JNI 引用
评论