JVM 内存、JMM内存与集群机器节点内存的联系
目录
1、JVM 内存
1.1、分配机制
1.2、jvm模型位置
1.3、字节码内存块
2、JMM内存
2.1、JMM模型
2.2、工作流程图
1、工作内存与主内存的交互
2. 多线程下的主内存与堆内存交互
2.3、 主内存与工作内存的同步方案
1、volatile
2、synchronized
3、final
3、内存使用
3.1、集群环境
3.2、容器化环境
3.3、示例场景
4、常见问题与解决方案
前言
在日常开发过程中,不知道你是否考虑JVM内存模型、JMM模型、计算机内存模型它们是通过何种机制来进行联系的。
问题:
比如当选择一台物理机器节点部署应用的时候,如何选择规模适度的内存大小?选择了计算机内存后,如何为程序应用选择JVM内存大小。
在jvm内存大小设定好,JMM模型是如何用jvm交互的呢?本篇将介绍下,它们的联系及内存分配。
1、JVM 内存
1.1、分配机制
JVM运行内存分配时,其最终由 操作系统 管理。
JVM 的内存参数(如 -Xms、-Xmx)只是告诉 JVM 它期望使用的内存范围,但实际使用的内存是 运行 JVM 的物理机器或虚拟机节点的内存。
如下图所示:
根据上面可以看出:
JVM内存模型是模仿操作系统内存模型构建的,JVM内存模型和操作系统内存模型是可以一一对应起来的。
1.2、jvm模型位置
关于jvm内存模型,可参考:关于对JVM的知识整理_jvm知识-CSDN博客
JVM就类似于一个操作系统,整个JVM内存模型存储在操作系统的堆中。
如下图所示:
1、方法区:
而JVM的方法区,也就相当于操作系统/主机的硬盘区,也叫永久区;而操作系统栈(本地方法栈)和JVM的栈也是一致的;
2、堆:
JVM堆和操作系统堆在概念上和目标上是一致的,分配内存的方式也是一致的,但JVM堆管理垃圾的方式是GC回收,而操作系统堆则是需要程序员手动释放;
3、PC寄存器:
计算机上的PC寄存器是计算机上的硬件(是CPU内部用来存放“伪指令”或地址数据的一些小型存储区域)。
而对于虚拟机,PC寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),存放的是将要执行指令的地址。
1.3、字节码内存块
关于更多方法区的介绍,可参考:关于对JVM的知识整理_jvm知识-CSDN博客
ClassLoader这个类加载器存放在堆内存,当一个classLoder启动的时候,它会去主机硬盘上将A.class加载到jvm的方法区。
如下所示:
方法区里面的字节文件会被虚拟机读取并执行new A字节码(),然后在堆内存生成了一个A字节码的对象。
此时方法区里面A字节码内存文件有两个引用:
一个指向A的class对象,一个指向加载自己的classLoader引用。
如下图。
⚠️注意:图里面的字段信息应该是字段的结构信息(方法区),而不是字段的值(堆内存)。
对比字节码内存块和类的信息。
代码示例:
public final class ClassStruct extends Object implements Serializable {// 实例变量的值(存储在堆内存)// 实例变量的(结构信息存放在方法区)private String name;private int id;// 静态常量(存储在方法区)public final int CONST_INT = 0;public final String CONST_STR = "CONST_STR";// 静态变量(存储在方法区)public static String static_str = "static_str";// 静态方法(字节码存储在方法区)public static final String getStatic_str() throws Exception {return ClassStruct.static_str;}
}
结论:jvm方法区里面的字节码内存就是将完整的类信息加载到了内存。
参考类的信息:
ClassStruct
├── 类名: "ClassStruct"
├── 父类: "java.lang.Object"
├── 接口: "java.io.Serializable"
├── 字段:
│ ├── name: private java.lang.String
│ ├── id: private int
│ ├── static_str: public static java.lang.String
│ ├── CONST_INT: public final int
│ └── CONST_STR: public final java.lang.String
├── 方法:
│ └── getStatic_str(): public static final
└── 常量池:├── "static_str"├── "CONST_STR"└── 0
1.类信息:修饰符(public final)
是类还是接口(class,interface)
类的全限定名(Test/ClassStruct.class)
直接父类的全限定名(java/lang/Object.class)
直接父接口的权限定名数组(java/io/Serializable)
public final class ClassStruct extends Object implements Serializable这段描述的信息提取
2.字段结构信息:修饰符(pirvate)
字段类型(java/lang/String.class)
字段名(name)
private String name;这段描述信息的提取。(实例变量的值存放在堆内存里面)
3.方法信息:修饰符(public static final)
方法返回值(java/lang/String.class)
方法名(getStatic_str)
参数需要用到的局部变量的大小还有操作数栈大小(操作数栈)
方法体的字节码(就是花括号里的内容)
异常表(throws Exception)
public static final String getStatic_str ()throws Exception的字节码的提取
4.常量池:
整型直接常量池public final int CONST_INT=0;
字符串直接常量池 public final String CONST_STR="CONST_STR";
浮点型直接常量池
方法名、方法描述符、类名、字段名,字段描述符的符号引用
5.类变量:
就是静态字段( public static String static_str="static_str";)
虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。
6.一个到堆内存classLoader的引用:
通过this.getClass().getClassLoader()。
7.一个到堆内存class A对象的引用:
这个对象存储了所有这个字节码内存块的相关信息。
可使用反射:java的反射详解_java中的反射-CSDN博客
1、类信息,你可以通过this.getClass().getName()取得;
2、所有的方法信息,可以通过this.getClass().getDeclaredMethods()。
3、字段信息可以通过this.getClass().getDeclaredFields()。
2、JMM内存
2.1、JMM模型
Java 内存模型(Java Memory Model, JMM)定义了多线程环境下变量的可见性、原子性和有序性规则。
如下图所示:
Java 内存模型(JMM)
├── 主内存(所有线程共享)
│ ├── 堆内存(对象实例、数组)
│ ├── 方法区(类元数据信息、常量池、静态变量、字节码文件)
│ └── 静态变量
└── 工作内存(每个线程私有)├── 虚拟机栈(局部变量)└── 本地方法栈(Native 方法使用)
1、堆内存与JMM 主内存的联系
JVM 堆内存是 Java 内存模型(JMM)中主内存的一部分。
堆内存中的对象属于主内存:
所有在堆中创建的对象(如 new object())存储在 JMM 的主内存中。多线程共享这些对象,线程通过工作内存(如 CPU 缓存)读写堆内存中的对象。
JMM 的主内存范围更广:
主内存 不仅包含堆内存,还包括:
方法区(存储类信息、静态方法和字段、常量池、字节码文件等,JDK 8 后移至元空间 Metaspace)。
静态变量(存储在方法区)。
局部变量(存储在虚拟机栈中,但变量引用的对象存储在堆中)。
主内存 vs 工作内存:
主内存:所有线程共享的物理内存(包括堆、方法区等)。
工作内存:每个线程私有的本地内存(如 CPU 寄存器、高速缓存),用于临时存储变量副本。
注意:堆内存 是主内存中用于存储对象实例的核心部分。
总结
2.2、工作流程图
1、工作内存与主内存的交互
2. 多线程下的主内存与堆内存交互
1、变量读写流程
线程访问变量:
如果变量在主内存(如堆中的对象字段),线程会将变量复制到工作内存。修改后,线程将更新后的值写回主内存(通过 happens-before规则保证可见性)。
2、内存可见性问题
- 默认情况下,线程对变量的修改对其他线程不可见(因为工作内存和主内存的同步延迟)。
- 解决方案:
- 使用 volatile 关键字:强制每次读写都直接访问主内存。
- 使用 synchronized 或 Lock:通过锁机制确保内存同步。
代码示例:
public class JMMExample {private int sharedVariable = 0; // 存储在堆内存(主内存)public void increment() {int localVariable = this.sharedVariable; // 从主内存读取到工作内存localVariable++;this.sharedVariable = localVariable; // 将修改后的值写回主内存}
}
- 线程 A 执行 increment():
- 从主内存(堆)读取 sharedVariable 到工作内存。
- 修改后写回主内存。
- 线程 B 读取 sharedVariable:
- 如果未使用 volatile 或 synchronized,可能读取到旧值(工作内存未同步)。
2.3、 主内存与工作内存的同步方案
1、volatile
关于volatile的实现详细可参考:对于Synchronized和Volatile的深入理解_java sychronized和volatile以及同步代码块-CSDN博客
private volatile int sharedVariable = 0;
- 作用:禁止线程缓存变量副本,每次读写直接操作主内存。
2、synchronized
关于synchronized的实现可参考:对于Synchronized和Volatile的深入理解_java sychronized和volatile以及同步代码块-CSDN博客
public synchronized void increment() {sharedVariable++;
}
- 作用:通过锁确保变量的修改对其他线程可见。
3、final
关于final的具体应用,可参考:对于final、finally和finalize不一样的理解-CSDN博客
private final int sharedVariable = 0;
- 作用:final 变量在构造函数结束后对其他线程可见。
3、内存使用
3.1、集群环境
在 集群部署(如 Kubernetes、Docker Swarm、物理服务器集群)中,每个 JVM 实例运行在某个 节点(Node) 上。
JVM 的内存分配规则如下:
1、节点物理内存 vs JVM 堆内存
1.JVM 堆内存(Heap Memory):
通过 -Xms(初始堆大小)和-Xmx(最大堆大小)参数配置。这些参数控制的是 JVM 堆内存,但它会占用 所在节点的物理内存。
例如:-Xmx4g
表示 JVM 最多可以使用 4GB 节点内存用于堆。
2.非堆内存(Metaspace、Direct Memory 等):
Metaspace(类元数据):默认无上限(需通过 -XX:
MaxMetaspaceSize 限制)。
直接内存(Direct Memory):通过 -XX:
MaxDirectMemorySize 配置,同样占用节点物理内存。
如下图所示:
3.2、容器化环境
如Docker/Kubernetes。
1、容器内存限制:
如果 JVM 运行在容器中(如 Docker 容器),容器的内存限制(如 --memory=
4g 或 Kubernetes 的 resources.limits.memory)会 限制 JVM 可使用的总内存。
JVM 无法突破容器内存限制,否则会被操作系统强制终止(OOMKilled)。
2、JVM 参数与容器限制的关系:
-Xmx 应小于容器内存限制的 70%-80%,为其他组件(如非堆内存、OS 缓存)预留空间。例如:容器内存限制为 4GB,则 -Xmx 建议设为 3g
。
3.3、示例场景
1:物理服务器集群
- 节点配置:8GB 内存。
- JVM 配置:-Xmx
4g
。 - 实际内存使用:JVM 最多占用 4GB 节点内存(堆内存),其余内存可用于其他服务(如数据库、中间件)。
2:Kubernetes 集群
- Pod 配置:resources.limits.memory
=
4Gi。 - JVM 配置:-Xmx3g。
- 实际内存使用:JVM 最多占用 3GB 容器内存,剩余 1GB 用于非堆内存和容器内其他进程。
4、常见问题与解决方案
1、JVM 报 OOM 但节点内存充足
- 原因:
- JVM 堆内存未配置,导致堆内存无限制。
- 非堆内存(如 Metaspace)未限制,导致内存泄漏。
- 解决方案:
- 显式设置 -Xmx 和 -XX
:
MaxMetaspaceSize。 - 监控 JVM 内存使用(如通过 Prometheus + Grafana)。
- 显式设置 -Xmx 和 -XX
2、容器被 OOMKilled
- 原因:
- JVM 堆内存 + 非堆内存 + 容器其他进程内存总和超过容器限制。
- 解决方案:
- 降低 -Xmx,为非堆内存和系统开销预留空间。
- 使用容器内存配额(如 Kubernetes 的 resources.limits.memory)。
参考文章:
1、JVM内存是对应到操作系统内存_jvm内存和电脑内存的关系-CSDN博客
2、关于对JVM的知识整理_jvm知识-CSDN博客