1. JVM 内存区域 学习笔记
1.1. JVM 内存区域
JVM将内存划分为不同区域主要是为了高效管理、安全隔离和性能优化,这种设计是经过长期实践验证的架构选择。以下是内存区域划分的核心原因:
职责分离原则
- 不同数据有不同的生命周期和使用方式(如类信息与对象实例)
- 分离存储便于针对性管理(如GC策略不同)
性能优化
- 热点数据单独存放(如栈上的局部变量快速访问)
- 减少内存碎片(堆与栈使用不同分配策略)
安全隔离
- 防止用户代码直接操作方法区等关键区域
- 线程私有数据避免并发问题(如程序计数器)
JVM(Java虚拟机)内存区域是Java程序运行时数据存储的关键部分,了解这些区域对于性能调优和故障排查至关重要。JVM内存主要分为以下几个区域(方法区、程序计数器、Java虚拟机栈、Java堆内存):
1.1.1. 方法区(Method Area)
该区域主要 存放 从“.class”文件里加载进来的类。(包括一些常量池等数据)
总结
- 线程共享:存储已被虚拟机加载的类型信息
- 内容:
- 类型信息(类名、访问修饰符等)
- 常量池(Runtime Constant Pool)
- 字段描述
- 方法描述
- 方法字节码
- 类静态变量
- 实现变化:
- JDK 7及之前:永久代(PermGen)
- JDK 8+:元空间(Metaspace,使用本地内存)
- 异常:OutOfMemoryError
1.1.2. 运行时常量池(Runtime Constant Pool)
总结
- 位置:方法区的一部分
- 内容:
- 类文件常量池的运行时表示
- 符号引用(Symbolic References)
- 字面量(Literals)
- 特点:
- 动态性:运行时可以将新的常量放入池中
- 如String.intern()方法
1.1.3. 程序计数器(Program Counter Register)
计算机不能直接识别代码,如果想让计算机执行代码逻辑,先要把代码转换成字节码(.java -> .class)。
.class
文件中就是编译出来的字节码,文件内容就是一条条机器指令(eg:从内存读取某个数据,或者把某数据写到内存等,各种各样指令的集合)。
我们写好的Java代码会被翻译成字节码,对应各种字节码指令。JVM加载类信息到内存之后,会使用字节码执行引擎,去执行我们写的代码编译出来的代码指令。
在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器”。
程序计数器就是用来记录当前执行的字节码指令的位置的,也就是记录目前执行到了哪一条字节码指令(每个线程都有自己的程序计数器)。
总结
- 线程私有:每个线程都有独立的程序计数器
- 作用:记录当前线程执行的字节码指令地址
- 特点:
- 执行Java方法时记录虚拟机字节码指令地址
- 执行Native方法时值为空(Undefined)
- 唯一不会出现OutOfMemoryError的内存区域
1.1.4. Java虚拟机栈(Java Virtual Machine Stacks)
Java代码在执行的时候,一定是线程来执行某个方法中的代码。在方法里,我们经常会定义一些方法内的局部变量。
因此,JVM必须有一块区域是来保存每个方法内的局部变量等数据,这个区域就是Java虚拟机栈。(每个线程都有自己的Java虚拟机栈)
如果线程执行了一个方法,就会对这个方法调用创建对应的一个栈帧。栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等。
使用下列代码和画图,方便理解记忆:
1. 执行main()
方法
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
在执行上面代码的时候,就会通过main线程对应的程序计数器记录自己执行的指令位置。该线程会有自己的 Java 虚拟机栈,用来保存执行的方法及对应的局部变量replicaManager
。
2. 当main()
方法继续执行ReplicaManager对象里面的方法时
public class ReplicaManager {
public void loadReplicasFromDisk() {
Boolean hasFinishesLoad = false;
if (isLocalDataCorrupt()) {
System.out.println("执行指定逻辑。。。");
}
}
public boolean isLocalDataCorrupt() {
boolean isCorrupt = false;
return isCorrupt;
}
}
当执行到loadReplicasFromDisk()
方法时,会把该方法及对应局部变量等,压入虚拟机栈中。
继续执行代码,发现又调用了isLocalDataCorrupt()
方法,还是继续上述操作,将该方法及局部变量等压入虚拟机栈中。
3. 方法执行完后的操作
当isLocalDataCorrupt()
方法执行完毕后,会将isLocalDataCorrupt()
方法对应的栈帧从 Java 虚拟机栈中出栈。
继续执行loadReplicasFromDisk()
方法,该方法执行完毕,将该方法的栈帧出栈。按此步骤直到所有方法都执行完毕。
总结
- 线程私有:生命周期与线程相同
- 作用:存储方法调用的栈帧(Stack Frame)
- 组成:
- 局部变量表:存储方法参数和局部变量
- 操作数栈:方法执行的工作区
- 动态链接:指向运行时常量池的方法引用
- 方法返回地址:方法执行完毕后的返回位置
- 异常:
- StackOverflowError:线程请求的栈深度超过限制
- OutOfMemoryError:无法申请到足够内存扩展栈
1.1.5. 本地方法栈(Native Method Stack)
总结
- 线程私有:与虚拟机栈类似
- 作用:为Native方法服务
- 特点:
- HotSpot虚拟机中与虚拟机栈合二为一
- 同样会抛出StackOverflowError和OutOfMemoryError
1.1.6. Java堆(Java Heap)
新创建对象的数据存储区域Java堆内存。堆内存的数据,线程之间都可以共享访问的。
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager {
private long replicaNum;
public void loadReplicasFromDisk() {
Boolean hasFinishesLoad = false;
if (isLocalDataCorrupt()) {
System.out.println("执行指定逻辑。。。");
}
}
public boolean isLocalDataCorrupt() {
boolean isCorrupt = false;
return isCorrupt;
}
}
上述代码main()
方法中new ReplicaManager()
会创建一个新的对象实例(包含一些数据,例如replicaNum
等)。该对象实例数据会放在 Java堆内存 中。
Java虚拟机栈 的局部变量replicaManager
是引用类型,存放ReplicaManager
对象的堆内存地址。(相当于局部变量表replicaManager
指向堆内存的ReplicaManager
对象)
总结
- 线程共享:所有线程共享的内存区域
- 作用:存储对象实例和数组
- 特点:
- 虚拟机启动时创建
- 垃圾收集器管理的主要区域(GC堆)
- 可以处于物理上不连续的内存空间
- 分代设计(大多数JVM实现):
- 新生代(Young Generation):
- Eden区
- Survivor区(From/To)
- 老年代(Old Generation/Tenured)
- 永久代/元空间(JDK 8+改为元空间)
- 新生代(Young Generation):
- 异常:OutOfMemoryError
1.1.7. 直接内存(Direct Memory)
总结
- 非JVM规范定义:但被广泛使用
- 特点:
- 不是JVM运行时数据区的一部分
- 通过Native函数库直接分配堆外内存
- 常见于NIO的DirectByteBuffer
- 优势:
- 减少Java堆和Native堆间的数据复制
- 提高IO性能
- 异常:OutOfMemoryError
1.1.8. 内存区域对比表
区域名称 | 线程共享性 | 是否GC | 异常类型 | 作用 |
---|---|---|---|---|
程序计数器 | 线程私有 | 否 | 无 | 指令地址 |
虚拟机栈 | 线程私有 | 否 | SOE/OOME | Java方法调用 |
本地方法栈 | 线程私有 | 否 | SOE/OOME | Native方法调用 |
Java堆 | 线程共享 | 是 | OOME | 对象实例存储 |
方法区 | 线程共享 | 是 | OOME | 类信息、常量池等 |
运行时常量池 | 线程共享 | 是 | OOME | 类文件常量池的运行时表示 |
直接内存 | - | - | OOME | 堆外内存 |
1.2. JVM 核心内存区域全流程图
public class Kafka {
public static void main(String[] args) {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.loadReplicasFromDisk();
}
}
public class ReplicaManager {
private long replicaNum;
public void loadReplicasFromDisk() {
Boolean hasFinishesLoad = false;
if (isLocalDataCorrupt()) {
System.out.println("执行指定逻辑。。。");
}
}
public boolean isLocalDataCorrupt() {
boolean isCorrupt = false;
return isCorrupt;
}
}
上面代码执行时整体流程如下(附图):
- 启动 JVM 进程,会把相关类加载到方法区内存。然后会有 main 线程来执行
main()
方法。 - main 线程关联了一个程序计数器(如果有多个线程执行这段代码,每个线程有自己的程序计数器)。线程执行代码到某个位置,程序计数器都会记录。
- 在 main 线程执行程序的时候,会有自己的 Java 虚拟机栈,用来存储正在执行的方法及局部变量。
- 其中方法中如果有对象类型,局部变量存的是对象的地址,地址为 Java 堆内存位置(放置的是对象实例)。
- 当方法执行完后会从 Java 虚拟机栈中出栈,程序计数器会继续向下执行,直到所有方法执行完,整段代码结束。
1.3. 常见内存问题
堆内存溢出(OOM):
- java.lang.OutOfMemoryError: Java heap space
- 原因:对象太多或太大,无法被GC回收
- 解决:调整-Xmx/-Xms参数,分析内存泄漏
栈溢出(SOE):
- 现象:java.lang.StackOverflowError
- 原因:递归调用过深或栈帧过大
- 解决:调整-Xss参数,优化递归算法
元空间溢出:
- 现象:java.lang.OutOfMemoryError: Metaspace
- 原因:加载类过多(如动态生成类)
- 解决:调整-XX:MetaspaceSize/-XX:MaxMetaspaceSize
直接内存溢出:
- 现象:java.lang.OutOfMemoryError: Direct buffer memory
- 原因:分配过多堆外内存
- 解决:调整-XX:MaxDirectMemorySize
1.4. JVM内存参数调优
理解JVM内存区域是Java性能调优的基础,合理配置内存参数可以显著提高应用性能和稳定性。
常用参数示例:
java -Xms512m -Xmx1024m -Xss256k -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m ...
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -Xss:线程栈大小
- -XX:MetaspaceSize:元空间初始大小
- -XX:MaxMetaspaceSize:元空间最大大小