JVM内存模型(堆、栈、方法区)
学习目标:深入理解JVM内存模型中的堆、栈和方法区
文章目录
目录
[二、JVM Stacks(栈)](#二、JVM Stacks(栈))
[2.栈帧(Stack Frame)结构](#2.栈帧(Stack Frame)结构)
[局部变量表(Local Variable Table)](#局部变量表(Local Variable Table))
[操作数栈(Operand Stack)](#操作数栈(Operand Stack))
[动态链接(Dynamic Linking)](#动态链接(Dynamic Linking))
[方法返回地址(Return Address)](#方法返回地址(Return Address))
1.线程共享:JVM中最大的一块内存区域,所有线程都共享此区域。
2.存储内容:对象实例、数组、字符串常量池(JDK7+)、静态变量(从JDK7起)
[即时编译代码缓存(JIT Cache):热点方法编译后的机器码](#即时编译代码缓存(JIT Cache):热点方法编译后的机器码)
[3. 历史演变](#3. 历史演变)
一、堆、栈和方法区的比喻:工厂车间
在开始之前,我们先建立一个宏观印象:
-
堆 :就像一个巨大的中央仓库,存放着所有生产出来的"产品"(对象实例)。大家都可以申请使用这里的空间。
-
栈 :就像每个工人手边的工作台,工作台上放着当前正在组装的零件(局部变量)。每个工人(线程)都有自己的工作台,互不干扰。
-
方法区 :就像工厂的设计图纸库,存放着所有产品的设计图(类信息、常量、静态变量)。
二、JVM Stacks(栈)
1.核心概念
1.线程私有:每个线程仔创建时都会同步创建一个虚拟机栈。生命周期与线程相同。
2.存储内容:存储栈帧(Stack Frame),每个栈帧对应一个方法的执行。
3.作用:描述Java方法执行的内存模型。每个方法从调用大执行完毕,都对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 入栈:方法调用时创建一个新的栈帧压入栈顶。
- 出栈:方法返回时(包括正常return或异常抛出)弹出当前栈帧。
2.栈帧(Stack Frame)结构
局部变量表(Local Variable Table)
- 存放方法参数和方法内部定义的局部变量
- 存储数据类型:
- 基本数据类型(
int,boolean等) - 引用类型(对象引用指针,如
Object)
- 基本数据类型(
- 大小固定:编译期确定大小(不会运行时扩大)
- Slot结构:
- 每个槽位(Slot)占32位(4字节)
long/double需占用2个连续Slot
操作数栈(Operand Stack)
- 临时工作区(类似CPU寄存器)
- 实现字节码指令的运算(如
iadd加法操作需从栈顶取2个值) - 后进先出结构:所有计算基于栈顶元素进行
动态链接(Dynamic Linking)
- 指向运行时常量池的符号引用(如
com/example/MyClass#method()) - 指向运行时转换为直接引用(方法实际地址)
作用:
- 支持多态(虚方法绑定)
- 解决跨类方法调用的引用确定问题
方法返回地址(Return Address)
- 存储方法退出后下一条指令的位置
- 方法退出方式:
- 正常退出:return指令(PC计数器记录地址)
- 异常退出:异常表记录处理地址
- 异常处理路径:
- 若方法未处理异常 → 栈帧被弹出并传递给调用方
- 若所有栈帧均未处理 → 线程终止
详细例子分析:
java
public clas StackExample{
public static void main(String[] args){
int a = 10;
int b = 20;
int result = add(a,b);
System.out.println(result);
}
public static int add(int x, int y){
int sum = x + y;
return sum;
}
}
分析栈的变化:
1,程序启动:main线程启动,JVM为其创建虚拟机栈。
2,执行main方法:main方法的栈帧被压入栈。
局部变量表:存储args, a, b, result。
此时栈的状态:
java
|--------------------|
| main方法的栈帧 | <-- 栈顶
|--------------------|
3,调用add方法:在计算add(a, b)时,add方法的栈帧被压入栈。
局部变量表:存储参数 x (值为10),y (值为20),以及局部变量sum.
此时栈的状态:
java
|--------------------|
| add方法的栈帧 | <-- 栈顶
|--------------------|
| main方法的栈帧 |
|--------------------|
4,add方法执行完毕:add方法执行到return sum; 其栈帧出栈。返回值被传递给main方法栈帧中的result变量。
此时栈的状态:
java
|--------------------|
| main方法的栈帧 | <-- 栈顶
|--------------------|
5,main方法执行完毕:main方法栈帧出栈,栈空,线程结束。
栈的异常
StackOverflowError:
如果线程请求的栈深度大于虚拟机允许的深度(比如无限递归),就会抛出此错误。
java
public class StackOverflowDemo{
public static void recursiveMethod(){
recursiveMethod();//无限递归自己
}
public static void main(String[] args){
recursiveMethod();
}
}
OutOfMemoryError:
如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存,就会报此错误。
三,Heap(堆)
核心概念
1.线程共享:JVM中最大的一块内存区域,所有线程都共享此区域。
为规避线程安全问题,JVM设计了 TLAB(Thread-Local Allocation Buffer) 机制:
- 每个线程在Eden区有独立的分配缓冲区
- 小对象优先在TLAB分配(无需锁)
- 大对象直接分配在共享堆空间(需同步)
2.存储内容:对象实例、数组、字符串常量池(JDK7+)、静态变量(从JDK7起)
3.GC策略:垃圾收集器管理的主要区域,被称为"GC堆"。
补充机制:
- 分代收集理论:年轻代(Minor GC)与老年代 (Major GC).
- GC类型触发条件:
- Eden满 ------> Minor GC
- 老年代满 ------> Maior GC
- Metaspace满/System.gc()------> Full GC
4.分区结构:分代结构是堆的核心设计。
- 新生代(Eden + Survivor S0/S1)
- 老年代(对象长期存活区)
关键分区解析:

- 转移规则 :
- 对象在Eden分配
- Minor GC存活 → 移入Survivor区(年龄+1)
- 年龄达阈值(默认15)→ 晋升老年代
扩展说明:JDK8+永久代被元空间(Metaspace)替代,堆只存储对象数据
解决的问题:掌握分代设计与GC触发机制,是解决OOM、优化高并发应用内存的关键基础!
详细例子:
java
public class HeapExample{
public static void main(String[] args){
Person person = new Person("Alice", 25);
}
}
class Person{
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
}
内存分配过程:
1,Person person :这会在当前线程(main) 的栈帧的局部变量表中创建一个person变量。这个变量是一个引用类型,它现在还没有指向任何对象,值是null.
2,new Person("Alice", 25): new关键字会在堆中开辟一块内存空间,用于存储新创建的Person对象。这个对象包含了其内部数据name和age。
注意:String name 本身也是一个对象。字符串"Alice" 如果不存在,可能会在堆内的字符串常量池(JDK7后移至堆)中创建。
3,= :赋值操作将栈中的person引用指向了堆中的Person对象实例。
最终内存结构图:
java
栈 (Stack) 堆 (Heap)
|----------------| |-------------------|
| main栈帧 | | |
| ... | | ... |
| person (引用) --------→ | Person对象实例 |
| | | - name (引用) → | "Alice" (String对象) |
|----------------| | - age = 25 |
|-------------------|
5.堆的异常
- OutOfMemoryError:当堆中没有足够的内存完成实例分配,并且堆也无法再扩展时,抛出此错误。这是最常见的内存错误之一。
四,方法区
1.核心概念
1,线程共享:与堆一样,是各个线程共享的内存区域。
2,存储内容:
- 类信息 (Class Metadata):
- 类全限定名(如
java.lang.String) - 方法字节码
- 字段定义
- 类全限定名(如
- 运行时常量池 (Runtime Constant Pool):
- 符号引用(
#12 = Methodref) - 字面量(
String s="hello"的"hello"引用)
- 符号引用(
即时编译代码缓存(JIT Cache):热点方法编译后的机器码
3. 历史演变
- JDK7及之前:永久代(PermGen)作为方法区实现(在堆内)
- JDK8+ :元空间(Metaspace)替代永久代
- 存储位置:本地内存(Native Memory)
- 关键优势:自动扩容(无
PermGen OOM)
4.元空间(Metaspace)进阶详解
元空间 vs 永久代
| 特性 | 永久代(PermGen) | 元空间(Metaspace) |
|---|---|---|
| 存储位置 | 堆内存内 | 本地内存(非堆) |
| 内存上限 | -XX:MaxPermSize=256m |
默认无上限(受OS限制) |
| OOM风险 | 高频(类加载过多) | 显著降低(仍可能OS耗尽) |
| 垃圾回收触发 | Full GC时回收 | 独立于堆GC |
详细例子:
java
public class MethodAreaExample{
//静态变量------>存储在方法区
pbulic static String STATIC_FIELE = "我是一个静态变量";
//常量------> 存储在方法区的运行是常量池
public static final String CONSTANT_FIELD = "我是一个常量";
public static void main(String[] args){
//类信息 -> 存储在方法区
//当第一次使用这个类时,JVM会加载它,并将其他类信息(类目,方法代码,字段信息等)存入方法区中。
//调用静态方法
printMessage();
}
public static void printMessage(){
//方法的字节码------> 存储在方法区
System.out.println(STATIC_FIELD);
System.out println(CONSTANT_FIELD);
}
}
5.内存结构图:
java
栈 (Stack) 堆 (Heap) 方法区 (Method Area)
|----------------| |-------------------| |-------------------------|
| main栈帧 | | | | MethodAreaExample类信息 |
| ... | | ... | | - 方法字节码 (main, printMessage) |
|----------------| |-------------------| | - 静态变量 STATIC_FIELD (引用) |
| "我是一个静态变量" | ←-----| - 常量 CONSTANT_FIELD (引用) → | "我是一个常量" |
| "我是一个常量" | |-------------------------|
|-------------------|
方法区的异常
- OutOfMemoryError:在JDK8之前,如果加载的类过多(比如大量动态生成类),可能导致永久代内存溢出。在元空间中,如果本地内存不足,同样会抛出此错误
五,总结与对比
| 特性 | 虚拟机栈 | Java堆 | 方法区 |
|---|---|---|---|
| 线程共享性 | 线程私有 | 线程共享 | 线程共享 |
| 存储内容 | 栈帧、局部变量、操作数栈 | 对象实例、数组 | 类信息 、常量、静态变量、JIT代码 |
| 生命周期 | 与线程相同 | 与JVM进程相同 | 与JVM进程相同 |
| 内存错误 | StackOverflowError OutOfMemoryError | OutOfMemoryError (最常见) | OutOfMemoryError |
| 比喻 | 工人的工作台 | 中央仓库 | 设计图纸库 |
六,综合案例:
java
public class ComprehensiveDemo {
// 静态变量 -> 方法区
private static String staticName = "GlobalName";
// 实例变量 -> 随对象存在于堆
private int instanceId;
public ComprehensiveDemo(int id) {
this.instanceId = id;
}
public void printInfo() {
// 局部变量 -> 栈
String localInfo = "Id: " + this.instanceId;
System.out.println(localInfo);
System.out.println(staticName);
}
public static void main(String[] args) {
// 引用变量 'demo' 在栈中
// 对象 new ComprehensiveDemo(1) 在堆中
ComprehensiveDemo demo = new ComprehensiveDemo(1);
// 调用方法,创建方法栈帧
demo.printInfo();
}
}