Java程序员在遇到内存溢出、栈溢出或频繁GC等问题时,都需要深入理解JVM内存结构。本文将系统讲解运行时数据区与内存模型,帮助掌握内存管理的核心知识。

OutOfMemoryError
StackOverflowError
频繁Full GC
内存泄漏
线程安全问题
CPU 100%
这些问题最终都指向:
JVM内存
例如:
List list = new ArrayList<>();while (true) {
list.add(new User());
}
运行后:
java.lang.OutOfMemoryError: Java heap space
为什么会报错?
因为:
堆内存被占满了
所以学习 JVM,首先必须搞懂内存结构。
当 JVM 启动时,会创建运行时数据区(Runtime Data Area)。
官方结构图:
JVM Runtime Data Area 线程共享
┌─────────────────────┐
│ Heap │
├─────────────────────┤
│ Method Area │
└─────────────────────┘
线程私有
┌─────────────────────┐
│ Program Counter │
├─────────────────────┤
│ Java Stack │
├─────────────────────┤
│ Native Method Stack │
└─────────────────────┘
分为两大类:
所有线程共同拥有:
Heap(堆)
Method Area(方法区)
每个线程独立拥有:
程序计数器
虚拟机栈
本地方法栈
这是 JVM 最小的一块内存。
作用:
记录当前线程执行到哪条字节码指令
例如:
public void test() {
int a = 1;
int b = 2;
int c = a + b;
}
JVM执行过程:
0: iconst_1
1: istore_12: iconst_2
3: istore_24: iload_1
5: iload_2
6: iadd
7: istore_3
程序计数器记录:
当前执行位置
例如:
当前执行到第5条指令
因为 JVM 支持多线程。
例如:
线程A
线程B
线程C
CPU不断切换:
A → B → C → A → B
切换回来时必须知道:
上次执行到哪里
因此每个线程都必须拥有独立程序计数器。
这是面试最高频知识点之一。
每个线程启动时:
创建一个虚拟机栈
每调用一个方法:
创建一个栈帧(Stack Frame)
例如:
public static void main(String[] args) {
test();
}public static void test() {
int age = 18;
}
执行过程:
main()
│
▼
test()
栈结构:
┌──────────┐
│ test() │
├──────────┤
│ main() │
└──────────┘
每个方法对应一个栈帧。
┌────────────────────┐
│ Local Variables │
├────────────────────┤
│ Operand Stack │
├────────────────────┤
│ Dynamic Linking │
├────────────────────┤
│ Return Address │
└────────────────────┘
例如:
public void test() {
int age = 18;
String name = "Tom";
}
存放:
age
name引用
JVM计算使用。
例如:
int c = a + b;
执行过程:
push a
push b
add
pop
类似:
1
2
+
=
3
经典面试题。
代码:
public void test() {
test();
}
执行:
test()
└─ test()
└─ test()
└─ test()
...
栈不断增长:
┌──────┐
│test │
├──────┤
│test │
├──────┤
│test │
├──────┤
│test │
└──────┘
最终:
java.lang.StackOverflowError
服务于:
native
关键字修饰的方法。
例如:
public native void start0();
Thread源码:
start()
↓
start0()
最终进入:
C++
Linux
Windows API
执行。
JVM中最大的一块内存。
也是 GC 最主要工作区域。
所有对象:
new User()new ArrayList()new HashMap()
都在堆中。
例如:
User user = new User();
内存:
Stack
└─ user引用Heap
└─ User对象
代码:
User user = new User();
发生了什么?
检查类是否加载:
User.class
未加载:
ClassLoader加载
堆中分配内存:
Heap
┌─────────┐
│ User对象 │
└─────────┘
初始化对象:
name = null;
age = 0;
执行构造函数:
public User() {
this.age = 18;
}
返回引用:
user
保存到栈。
JDK8以前:
Permanent Generation(PermGen)
JDK8以后:
Metaspace(元空间)
类元数据:
public class User {
}
存放:
类名
字段信息
方法信息
字节码
运行时常量池
public class User { private String name; public void hello() { }
}
方法区保存:
User类结构
hello方法信息
name字段信息
属于方法区的一部分。
例如:
String s = "hello";
字符串常量:
"hello"
会进入:
String Constant Pool
例如:
String a = "abc";
String b = "abc";
实际上:
a
---> "abc"
/
b
共享同一个对象。
很多人把 JVM 内存结构和 JMM 混淆。
实际上:
解决:
内存如何划分
问题。
解决:
线程之间如何访问内存
问题。
假设:
private boolean flag = false;
线程A:
flag = true;
线程B:
while (!flag) {}
理论上:
线程B应该结束循环
但实际上:
可能永远循环
为什么?
因为 CPU 缓存。
Main Memory
│
┌──────────┴──────────┐
│ │
Thread A Thread B
Working Memory Working Memory
每个线程:
有自己的工作内存
不能直接访问其他线程内存。
执行流程:
主内存
↓
工作内存
↓
修改
↓
刷新主内存
代码:
private volatile boolean flag = false;
线程A:
flag = true;
JMM保证:
立即刷新主内存
线程B:
读取主内存最新值
因此:
while循环结束
例如:
count++;
实际上:
读取
+
1
写回
不是原子操作。
线程A修改变量:
flag = true;
线程B能够立即看到。
JVM和CPU会优化:
指令重排
JMM保证:
在规则范围内有序
程序计数器
虚拟机栈
本地方法栈
堆
方法区
栈:
线程私有
方法调用
局部变量
堆:
线程共享
对象实例
GC管理
递归过深
栈帧过多
导致栈空间耗尽。
堆内存不足
元空间不足
直接内存不足
JVM内存结构:
关注内存区域划分
JMM:
关注线程间共享变量访问规则
掌握运行时数据区与JMM后,便理解了JVM内存管理的核心基础。下一章将深入垃圾收集原理,涵盖对象判定、GC类型及主流收集器选择,这是面试与调优的关键内容。