当前位置: 首页 > news >正文

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():
    1. 从主内存(堆)读取 sharedVariable 到工作内存。
    2. 修改后写回主内存。
  • 线程 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 配置:-Xmx4g
  • 实际内存使用: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)。

2、容器被 OOMKilled

  • 原因
    • JVM 堆内存 + 非堆内存 + 容器其他进程内存总和超过容器限制。
  • 解决方案
    • 降低 -Xmx,为非堆内存和系统开销预留空间。
    • 使用容器内存配额(如 Kubernetes 的 resources.limits.memory)。


参考文章:

1、JVM内存是对应到操作系统内存_jvm内存和电脑内存的关系-CSDN博客

2、关于对JVM的知识整理_jvm知识-CSDN博客

相关文章:

  • aardio 继承与多态
  • 关于 WASM: WASM + JS 混合逆向流程
  • 7. TypeScript接口
  • Python数据结构与算法(6.1)——树
  • 鸿蒙网络编程系列53-仓颉版TCP连接超时分析示例
  • python中的文件操作处理:文本文件的处理、二进制文件的处理
  • Android音视频多媒体开源框架基础大全
  • 基于Docker实现frp之snowdreamtech/frps
  • window显示驱动开发—为 DirectX VA 2.0 扩展模式提供功能(一)
  • 【JVM】- 类加载与字节码结构1
  • Spring AI详细使用教程:从入门到精通
  • RabbitMQ缓存详解:由来、发展、核心场景与实战应用
  • ubuntu之坑(十四)——安装FFmpeg进行本地视频推流(在海思平台上运行)
  • 软件工程的实践
  • ffmpeg subtitles 字幕不换行的问题解决方案
  • Yarn与NPM缓存存储目录迁移
  • MySQL查询缓存深度剖析
  • ffmpeg rtmp推流源码分析
  • 3GPP协议PDF下载
  • 【信创-k8s】重磅-鲲鹏arm+麒麟V10离线部署k8s1.30+kubesphere4.1.3
  • 深圳福田做网站/开发一个app平台大概需要多少钱?
  • 自己搭建聊天平台/贵港seo关键词整站优化
  • 免费教做面食的网站/百度关键词排名查询工具
  • 河北省建设厅官方网站 官网/公司运营策划方案
  • 在线商城网站制作/新的数据新闻
  • r语言做网站/怎么免费建个人网站