Appearance
java内存模型
一、线程私有区域:每个线程独有的“执行上下文”
线程私有区域是线程隔离的,每个线程启动时会分配独立的内存空间,线程结束后自动回收,无需GC介入。包含以下三个部分:
1. 程序计数器(Program Counter Register)
- 作用:记录当前线程执行的字节码指令地址(或Native方法的入口地址)。
例如,当线程执行if-else
、循环
、方法调用
时,程序计数器会跟踪下一条要执行的指令,确保线程切换(如CPU时间片轮转)后能恢复到正确的执行位置。 - 特点:
- 线程私有,每个线程有自己的程序计数器,互不干扰。
- JVM规范中唯一没有
OutOfMemoryError
的区域(因为它的内存大小固定,仅存储指令地址,不会动态扩展)。
- 示例:
当线程执行int a = 1 + 2;
时,对应的字节码是iconst_1
(压入1)、iconst_2
(压入2)、iadd
(加法)、istore_1
(存储到局部变量表),程序计数器会依次指向这些指令的地址。
2. 虚拟机栈(Java Virtual Machine Stacks)
- 作用:描述方法调用的生命周期,每个方法调用会创建一个栈帧(Stack Frame),栈帧包含方法执行的所有上下文信息。当方法执行完毕(正常返回或抛出异常),栈帧会从虚拟机栈中弹出。
- 栈帧的结构(重点):
每个栈帧由以下四部分组成,自上而下压入虚拟机栈:- 局部变量表(Local Variable Table):
存储方法的局部变量(基本数据类型、对象引用、返回值地址)。- 局部变量表的容量以**槽位(Slot)**为单位,1个Slot占4字节(32位)。
- 基本数据类型中,
boolean
、byte
、char
、short
、int
、float
占1个Slot;long
、double
占2个Slot(因为它们是64位)。 - 对象引用(如
Object obj
)占1个Slot,存储的是对象在堆中的内存地址(或句柄,取决于JVM实现)。 - 示例:java局部变量表的槽位分配:
public void func(int a, long b, Object c) { int d = a + 1; // 局部变量d }
a
(1 Slot)、b
(2 Slot)、c
(1 Slot)、d
(1 Slot),共5个Slot。
- 操作数栈(Operand Stack):
方法执行时的临时数据栈,用于存储运算的操作数和结果。
例如,执行int d = a + 1;
时,会先将a
(从局部变量表取出)压入操作数栈,再压入1
,然后执行iadd
指令(弹出两个操作数,相加后将结果压回操作数栈),最后用istore_3
指令将结果存储到局部变量表的d
槽位。 - 动态链接(Dynamic Linking):
指向方法区中当前方法的符号引用(Symbolic Reference)。
Java源文件编译成字节码时,方法调用会被编译为符号引用(如invokevirtual #5
,其中#5
是常量池中的符号)。动态链接的作用是在运行时将符号引用转换为直接引用(Direct Reference,即方法的实际内存地址),这一过程称为链接(Linking)。 - 方法返回地址(Return Address):
记录方法执行完毕后,回到调用者的位置(如调用者的程序计数器地址)。
例如,main
方法调用func()
,func()
执行完毕后,需要回到main
方法中func()
调用后的下一条指令继续执行,方法返回地址就是这个位置。
- 局部变量表(Local Variable Table):
- 虚拟机栈的大小:
虚拟机栈的大小可以通过**-Xss
参数**设置(如-Xss128k
),默认值取决于JVM版本和操作系统(如HotSpot在64位系统下默认是1M)。 - 常见异常:
StackOverflowError
:当线程请求的栈深度超过虚拟机栈的最大容量(如递归调用无终止条件)。
示例:javapublic void recursive() { recursive(); // 无限递归,导致栈溢出 }
OutOfMemoryError
:当虚拟机栈无法动态扩展(如设置了固定大小)且没有足够内存分配新的栈帧时。
3. 本地方法栈(Native Method Stacks)
- 作用:与虚拟机栈类似,但为Native方法(非Java实现的方法,如C/C++编写的方法)服务。
- 特点:
- 线程私有,结构与虚拟机栈一致,但具体实现依赖于JVM(如HotSpot将本地方法栈与虚拟机栈合并为同一区域)。
- 同样会抛出
StackOverflowError
(栈深度超过限制)和OutOfMemoryError
(无法扩展)。
- 示例:
Java中的System.currentTimeMillis()
方法底层调用的是Native方法,其执行上下文会存储在本地方法栈中。
二、线程共享区域:所有线程共同访问的“全局内存”
线程共享区域是所有线程共享的,生命周期与JVM进程一致,只有JVM退出时才会释放。包含以下两个核心部分:
1. 堆(Heap):对象的“主战场”
- 作用:存储对象实例(
new
关键字创建的对象)和数组(如int[] arr = new int[10]
)。
根据JVM规范:“所有对象实例和数组都必须在堆中分配”(但现代JVM通过逃逸分析优化,可能将未逃逸的对象分配在栈上,减少堆压力)。 - 堆的结构(分代模型):
为了优化垃圾收集(GC)效率,堆通常分为年轻代(Young Generation)和老年代(Old Generation),年轻代又分为Eden区和两个Survivor区(From/To),比例通常为Eden:Survivor From:Survivor To = 8:1:1(可通过-XX:SurvivorRatio
调整)。- 年轻代(Young Generation):存储新创建的对象(存活时间短)。
- Eden区:对象初次分配的区域(如
new Object()
会先放到Eden区)。 - Survivor区:用于保存Minor GC(年轻代GC)后存活的对象。两个Survivor区交替使用(From区和To区),每次Minor GC时,Eden区和From区的存活对象会被复制到To区,然后交换From和To的角色(To区变为下一次的From区)。
- Eden区:对象初次分配的区域(如
- 老年代(Old Generation):存储存活时间长的对象(如缓存对象、单例对象)。
当对象在Survivor区存活超过最大存活次数(由-XX:MaxTenuringThreshold
设置,默认15次),会被晋升到老年代。此外,大对象(如大数组)可能直接进入老年代(避免频繁复制,通过-XX:PretenureSizeThreshold
设置阈值)。
- 年轻代(Young Generation):存储新创建的对象(存活时间短)。
- 堆的大小设置:
- 初始堆大小:
-Xms
(如-Xms2g
,表示初始堆大小为2GB)。 - 最大堆大小:
-Xmx
(如-Xmx4g
,表示最大堆大小为4GB)。
建议将-Xms
和-Xmx
设置为相同值,避免JVM频繁扩展堆内存(扩展过程会导致STW,即Stop-The-World)。
- 初始堆大小:
- 常见异常:
OutOfMemoryError: Java heap space
:堆中没有足够内存分配新对象(如创建大量对象不释放,或内存泄漏)。
示例:javaList<Object> list = new ArrayList<>(); while (true) { list.add(new Object()); // 无限添加对象,导致堆溢出 }
- 堆的优化技术:
- 逃逸分析(Escape Analysis):判断对象是否“逃逸”出方法(如是否被返回、是否被其他线程访问)。若未逃逸,JVM可以将对象分配在栈上(而非堆上),减少GC压力。
示例:javapublic void func() { Object obj = new Object(); // 未逃逸(未被返回,未被其他线程访问),栈上分配 System.out.println(obj); }
- 标量替换(Scalar Replacement):将对象分解为基本数据类型(如
Point
类的x
、y
字段),存储在局部变量表中,避免创建对象。
示例:javaclass Point { int x; int y; } public void func() { Point p = new Point(); // 标量替换后,x和y作为局部变量存储在栈上 p.x = 1; p.y = 2; }
- 逃逸分析(Escape Analysis):判断对象是否“逃逸”出方法(如是否被返回、是否被其他线程访问)。若未逃逸,JVM可以将对象分配在栈上(而非堆上),减少GC压力。
2. 方法区(Method Area):类的“元数据仓库”
- 作用:存储类的元数据(如类的名称、修饰符、字段、方法信息)、常量(如字符串常量、数字常量)、静态变量(
static
修饰的变量)、即时编译器(JIT)编译后的代码(如热点代码)。 - 方法区的演变(重点):
- JDK 1.7及之前:方法区的实现是永久代(PermGen),属于JVM堆的一部分,用
-XX:PermSize
(初始大小)和-XX:MaxPermSize
(最大大小)设置。
问题:永久代大小固定,容易出现OutOfMemoryError: PermGen space
(如加载大量类,如Spring应用、动态代理框架)。 - JDK 1.8及之后:永久代被**元空间(Metaspace)**取代,元空间不在JVM堆中,而是使用本地内存(Native Memory)。
优点:元空间的大小由本地内存决定,默认无上限(可通过-XX:MaxMetaspaceSize
设置最大值),避免了永久代的内存限制。
- JDK 1.7及之前:方法区的实现是永久代(PermGen),属于JVM堆的一部分,用
- 方法区的核心组成:
- 运行时常量池(Runtime Constant Pool):
是方法区的一部分,存储类加载时的常量(如字面量、符号引用)。
例如,String s = "abc"
中的"abc"
是字面量,会存储在运行时常量池;Class.forName("com.example.User")
中的"com.example.User"
是符号引用,会转换为直接引用(类的内存地址)。 - 字符串常量池(String Constant Pool):
是运行时常量池的子集,存储字符串字面量(如"abc"
)。
演变:JDK 1.7之前,字符串常量池在永久代;JDK 1.7及之后,移到堆中(避免永久代溢出)。
示例:javaString s1 = "abc"; // "abc"存储在字符串常量池 String s2 = new String("abc"); // 堆中创建新对象,s2指向堆,s1指向常量池 System.out.println(s1 == s2); // false(地址不同) System.out.println(s1.equals(s2)); // true(内容相同)
- 运行时常量池(Runtime Constant Pool):
- 方法区的大小设置:
- 元空间初始大小:
-XX:MetaspaceSize
(如-XX:MetaspaceSize=256m
)。 - 元空间最大大小:
-XX:MaxMetaspaceSize
(如-XX:MaxMetaspaceSize=512m
)。
- 元空间初始大小:
- 常见异常:
OutOfMemoryError: Metaspace
:元空间无法扩展(如加载大量类,如动态生成类的框架CGLIB)。
示例:javawhile (true) { // 使用CGLIB动态生成类,导致元空间溢出 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(User.class); enhancer.create(); }
三、非运行时数据区:直接内存(Direct Memory)
- 作用:直接操作本地内存(而非JVM堆),用于优化IO性能(如NIO的
ByteBuffer.allocateDirect()
)。 - 特点:
- 不在JVM运行时数据区中,但属于Java程序的内存范围。
- 大小由本地内存决定,默认无上限(可通过
-XX:MaxDirectMemorySize
设置最大值)。 - 优点:减少Java堆与本地内存之间的复制(如文件读写时,直接内存无需拷贝到堆中),提高性能。
- 常见异常:
OutOfMemoryError: Direct buffer memory
:直接内存分配超过最大值(如创建大量直接缓冲区)。
示例:javawhile (true) { ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 分配1MB直接内存 }
四、JVM内存模型的核心总结
区域名称 | 线程共享性 | 存储内容 | 异常类型 | 大小设置参数 |
---|---|---|---|---|
程序计数器 | 私有 | 字节码指令地址 | 无 | 固定(无需设置) |
虚拟机栈 | 私有 | 方法栈帧(局部变量表、操作数栈等) | StackOverflowError、OutOfMemoryError | -Xss |
本地方法栈 | 私有 | Native方法栈帧 | StackOverflowError、OutOfMemoryError | (HotSpot与虚拟机栈合并) |
堆 | 共享 | 对象实例、数组 | OutOfMemoryError: Java heap space | -Xms、-Xmx |
方法区(元空间) | 共享 | 类元数据、常量、静态变量 | OutOfMemoryError: Metaspace | -XX:MetaspaceSize、-XX:MaxMetaspaceSize |
直接内存 | 共享 | 直接缓冲区(NIO) | OutOfMemoryError: Direct buffer memory | -XX:MaxDirectMemorySize |
五、常见内存问题排查思路
- 栈溢出(StackOverflowError):
- 原因:递归过深、方法调用链过长。
- 排查:查看异常栈 trace(找到递归方法),优化为迭代或增加栈大小(
-Xss
)。
- 堆溢出(OutOfMemoryError: Java heap space):
- 原因:内存泄漏(如集合持有对象引用不释放)、对象过多(如大数据处理)。
- 排查:
- 使用
jmap -dump:format=b,file=heap dump.hprof <pid>
导出堆 dump。 - 使用MAT(Memory Analyzer Tool)分析堆 dump,找到支配树(Dominator Tree)(占用内存最多的对象)。
- 示例:若
ArrayList
持有大量User
对象未释放,需检查是否有不必要的引用(如静态集合)。
- 使用
- 元空间溢出(OutOfMemoryError: Metaspace):
- 原因:加载大量类(如Spring、Hibernate、动态代理)。
- 排查:
- 使用
jcmd <pid> GC.class_stats
查看类加载统计。 - 增大元空间大小(
-XX:MaxMetaspaceSize
)或优化类加载(如使用类加载器卸载)。
- 使用
- 直接内存溢出(OutOfMemoryError: Direct buffer memory):
- 原因:创建大量直接缓冲区(如NIO程序)。
- 排查:增大直接内存大小(
-XX:MaxDirectMemorySize
)或减少直接缓冲区的使用。
六、JVM内存模型的实际应用
- 性能优化:
- 调整堆大小(
-Xms
、-Xmx
):根据应用类型(如Web应用、批处理应用)设置合适的堆大小,避免频繁GC。 - 调整年轻代比例(
-XX:SurvivorRatio
):若应用有大量短期对象,可增大Eden区比例(如-XX:SurvivorRatio=10
,Eden:Survivor=10:1)。 - 启用逃逸分析(
-XX:+DoEscapeAnalysis
):默认开启,减少堆分配。
- 调整堆大小(
- 内存泄漏排查:
- 使用
jconsole
或VisualVM
监控堆内存使用情况(如老年代持续增长)。 - 使用
jstack
查看线程栈(如线程阻塞导致对象无法释放)。
- 使用
总结
JVM内存模型是Java程序运行的基础,理解其结构和工作原理是解决内存问题、优化性能的关键。核心要点包括:
- 线程私有区域(程序计数器、虚拟机栈、本地方法栈):管理线程执行的上下文,生命周期与线程一致。
- 线程共享区域(堆、方法区):管理对象和类的元数据,生命周期与JVM一致。
- 直接内存:优化IO性能,不属于JVM运行时数据区,但需注意内存限制。
通过合理设置JVM参数(如-Xms
、-Xmx
、-Xss
、-XX:MaxMetaspaceSize
)和使用排查工具(如MAT、VisualVM),可以有效解决内存问题,提升Java应用的性能和稳定性。