目錄
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博客