JVM内存结构
JVM的组成的运行时数据区
JVM 由三大部分组成:
- 类加载器
- 运行时数据区
- 执行引擎

运行数据区也分五个小区域,两大类
| 区域名称 | 线程共享 | 线程私有 | 用途说明 |
|---|---|---|---|
| 程序计数器 | ✅ | 当前执行指令位置 | |
| Java 虚拟机栈 | ✅ | 方法调用过程中的数据存储 | |
| 本地方法栈 | ✅ | 本地方法调用 | |
| 堆(Heap) | ✅ | 所有对象存储 | |
| 方法区/元空间 | ✅ | 类信息、常量、静态变量 |
| 内存区域 | 存储内容 | 生命周期 |
|---|---|---|
| 堆(Heap) | 所有对象实例、数组 | 和JVM同生共死 |
| 栈(Stack) | 每个线程的方法调用栈帧 | 随线程创建与销毁 |
| 方法区 | 类的结构、常量池、静态变量等 | 和JVM同生共死 |
| 本地方法栈 | Native方法调用信息 | 随线程创建与销毁 |
| 程序计数器 | 当前线程执行字节码的行号指示器 | 随线程创建与销毁 |
程序技术器
程序计数器是 JVM 中的一块非常小的内存空间,用于记录当前线程所执行字节码的行号地址。
- 因为在Java虚拟机的多线程环境下,为了支持线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,否则就会出现线程切换后执行位置混乱的问题。
- JAVA源代码会先编译成字节码指令,并由类加载器加载到方法区,程序计数器的作用就是记录下一步应该执行的字节码指令的地址
1
2
3
4
5
6
7
8
90 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #13 <java/io/PrintStream.println : (I)V>
14 return小结
程序计数器 - 作用:保存当前线程下一条要执行的指令的地址
- 特点:
- 线程私有
- 不存在内存溢出
虚拟机栈
定义
- Java虚拟机栈(Java Virtual Machine Stacks)是Java虚拟机为每个线程分配的一块内存区域,用于存储线程的方法调用和局部变量等信息。
- 每个线程在运行时都有自己的Java虚拟机栈,线程开始时会创建一个新的栈帧(Stack Frame),用于存储该线程的方法调用信息。当方法调用完成后,该栈帧会被弹出,回到上一次方法调用的位置。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
虚拟机栈(一个线程)
┌────────────┐ ← 栈顶
│ 方法C栈帧 │ ← 当前正在执行的方法
├────────────┤
│ 方法B栈帧 │
├────────────┤
│ 方法A栈帧 │ ← 最先被调用的方法(main)
└────────────┘ ← 栈底
当我们看到程序报错出现异常的时候,就会从上到下依次弹出虚拟机栈,所以第一个弹出的就是关键的报错异常
栈帧
一个栈帧保存一个方法执行过程中的所有信息,包括:
| 组件 | 说明 |
|---|---|
| 局部变量表 | 存储方法的参数和局部变量(int、long、Object 引用等) |
| 操作数栈 | 执行字节码指令时用来做计算和临时数据存储 |
| 动态链接 | 方法中调用其他方法时的符号引用信息(类似“函数指针”) |
| 返回地址 | 方法返回时跳转的字节码地址 |
| 附加信息 | 如异常处理表等,供调试/异常处理用 |
栈内存溢出
栈帧太多导致栈内存溢出
无限递归或者递归太深会导致栈帧过多会导致栈内存溢出
1 | |
递归最终停在23678次出现了栈内存溢出的异常
- 设置栈内存大小

单个栈帧过大
这个情况出现在 一个类A中引用了B类,B类中又引用了A类的时候,也就是循环引用的时候才会出现
本地方法栈
本地方法是指由非Java语言编写的代码,如C或C++,并被编译为本地二进制代码。
因为JAVA没法直接和操作系统底层交互,所以需要用到本地方法栈来调用本地的C或C++的方法
例如Object类的源码中就有本地方法,用native关键字修饰本地方法
本地方法只有函数声明,没有函数体,因为函数体是C或C++写的,通常是通过JNI(Java Native Interface)技术来实现的。
| 项目 | 虚拟机栈(JVM Stack) | 本地方法栈(Native Method Stack) |
|---|---|---|
| 作用 | 管理 Java 方法的调用和执行 | 管理 Native 方法的调用 |
| 内容 | 栈帧(方法局部变量、操作数栈等) | 本地方法所需的结构、变量 |
| 是否必须存在 | 是 | 不是所有 JVM 实现都支持(HotSpot 把它与 JVM Stack 合并) |
| 触发异常 | StackOverflowError、OutOfMemoryError | 同上,如果单独存在 |
堆
✅ 定义:
JVM 堆是所有线程共享的一块内存区域,用于存放对象实例、数组等,只要用 new 创建的对象,基本都在堆中分配内存。
Heap堆
- 通过new关键字创建的对象都会使用堆空间
- 特点:
- 它是线程共享的,堆空间内的对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆大小设置
可以通过 JVM 启动参数设置堆的初始大小和最大大小:
1 | |
-Xms: 初始堆大小
-Xmx: 最大堆大小
注意:堆太小,容易频繁 GC;堆太大,Full GC 时间可能过长
堆溢出(OutOfMemoryError)
当堆内存不够用时,会出现:
1 | |
常见原因:
- 创建过多对象且未释放;
- 内存泄漏(引用未置 null 导致 GC 无法回收);
- 数据结构过大(如 List 无限添加元素);
arthas 工具可查看堆内存使用情况
方法区
✅定义:
在Java虚拟机(JVM)中,方法区(Method Area)是内存的一部分,属于JVM运行时数据区的一种。它用于存储类的结构信息,而不是对象的实例数据。方法区在JVM中有时也被称为永久代(PermGen)(在JDK 7及以前版本中),在JDK 8中被称为元空间(Metaspace)。
✅方法区主要存储以下内容:
- 类的结构信息(Class信息):
- 类的完全限定名(类名 + 包名)
- 类的访问修饰符(public、abstract等)
- 父类名称
- 实现的接口
- 字段信息(字段名、类型、修饰符)
- 方法信息(方法名、参数、返回值、修饰符)
- 运行时常量池(Runtime Constant Pool):
- 字符串字面量(如 “Hello”)
- 数字常量、方法引用、字段引用等
- 静态变量
- 类的构造方法和普通方法的字节码
JVM会通过类加载器将类的信息加载到方法区,在类的加载期完成
PermGen与Metaspace的区别
| 特性 | PermGen(JDK 7及以前) | Metaspace(JDK 8及以后) |
|---|---|---|
| 所在内存 | JVM内存 | 本地内存(Native memory) |
| 是否容易内存溢出 | 是(PermGen space OOM) | 较少(可通过参数控制增长) |
| 可调参数 | -XX:PermSize,-XX:MaxPermSize | -XX:MetaspaceSize,-XX:MaxMetaspaceSize |
StringTable
StringTable 是一个专门存放 String.intern() 方法返回的字符串对象 的哈希表。
来看一段代码
1
2
3
4
5
6
7
8
9
10
11public class Demo_08 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
String s5 = "a" + "b";
System.out.println(s3 == s5);
}
}反编译结果如下,可以看到s5对象的创建,就是去常量池中直接获取ab,而不会创建新的字符串对象,故s3 == s5的结果是true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8中,将这个字符串对象尝试放入串池
- 如果串池中已有,则不会放入
- 如果串池中没有,则放入串池,并将串池中的结果返回
下面是示例代码讲解1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class Demo_10 {
public static void main(String[] args) {
String s1 = "a"; // 常量池:["a"]
String s2 = "b"; // 常量池:["a", "b"]
String s3 = "a" + "b"; // 常量池:["a", "b", "ab"]
String s4 = s1 + s2; // 堆:new String("ab")
String s5 = "ab"; // s5引用常量池中已有的对象
String s6 = s4.intern(); // 常量池中已有"ab",将常量池中的"ab"的引用返回,s6引用常量池中已有的对象
System.out.println(s3 == s4); // s3在常量池,s4在堆,false
System.out.println(s3 == s5); // s3在常量池,s5在常量池,true
System.out.println(s3 == s6); // s3在常量池,s6在常量池,true
String str1 = "cd"; // 常量池:["cd"]
String str2 = new String("c") + new String("d"); // 堆:new String("cd")
str2.intern(); // 常量池中已有"cd",放入失败
System.out.println(str1 == str2); // str1在常量池,str2在堆,false
String str4 = new String("e") + new String("f"); // 堆:new String("ef")
str4.intern(); // 常量池中没有"ef",放入成功,并返回常量池"ef"的引用
String str3 = "ef"; // 常量池:["ef"]
System.out.println(str3 == str4); // str4是常量池的引用,str3也是常量池的引用,true
}
}
- 1.8中,将这个字符串对象尝试放入串池







