一.概述 我們在進行 Java 開發的時候,很少關心 Java 的記憶體分配等等,因為這些活都讓 JVM 給我們做了。不僅自動給我們分配記憶體,還有自動的回收無需再占用的記憶體空間,以騰出記憶體供其他人使用。但是我們經常面臨的一個問題就是記憶體泄漏,JVM無法完成回收工作,導致記憶體占用暴漲,最後可能讓程式奔潰 ...
一.概述
我們在進行 Java 開發的時候,很少關心 Java 的記憶體分配等等,因為這些活都讓 JVM 給我們做了。不僅自動給我們分配記憶體,還有自動的回收無需再占用的記憶體空間,以騰出記憶體供其他人使用。但是我們經常面臨的一個問題就是記憶體泄漏,JVM無法完成回收工作,導致記憶體占用暴漲,最後可能讓程式奔潰。本章主要瞭解下運行時數據區域分佈情況以及溢出異常。
二.運行時數據區域
1、程式計數器
- 線程私有
- 當前線程所執行的位元組碼的行號指示器
- Java 多線程是通過再一個內核中輪流執行實現的,計數器就保證了切換線程的時候可以回到原來正確的執行位置
- 程式計數器必須每個線程單獨一個,是線程私有的記憶體區域
- 程式計數器是唯一一個 JVM 沒有規範 OutOfMemoryError 的區域
2、Java虛擬機棧(java方法)
- 線程私有
- Java方法執行的記憶體模型,即方法執行時會創建一個棧幀,保存了需要的局部變數表、操作數棧、動態鏈接、方法出口等信息;
- 線程請求的棧深度>JVM允許的深度時,報StackOverflowError;
- 大多數的 JVM 可以動態擴展記憶體,如果無法申請到足夠的記憶體時,報 OutOfMemoryError;
3、本地方法棧(native方法)
- 同Java虛擬機棧
4、Java堆
- 線程共用
- 唯一目的:存放對象實例
- 分類:新生代、老生代,或者 Eden 空間、From Survior 空間、To Survivor 空間
- 分類目的:更好的回收和分配記憶體
- 沒有記憶體完成實例分配,或者不能再擴展,報OutOfMemoryError 異常
- 可以自己配置大小(-Xmx和-Xms)
5、方法區
- 線程共用
- 目的:存儲類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據;
- 該區記憶體回收目標:主要針對常量池的回收和對類型的卸載;
- 無法滿足記憶體分配要求時,報 OutOfMemoryError 異常
6、運行時常量池
- 註意:運行時常量池屬於方法區
- 目的:存儲編譯期生成的各種字面量和符號引用
- 特征:並非只有編譯期置入 Class 文件中的常量池內容才能進入運行時常量池,在運行期間也可以置入新的常量,比如 String 的intern() 方法;
- 無法申請足夠記憶體時,報 OutOfMemoryError 異常
三.直接記憶體
- 非運行時數據區域記憶體
- Native 函數分配堆外記憶體,堆內的 DirectByteBuffer 作為這塊記憶體的引用
- 性能顯著提高,避免了 Java 堆和 native 之間來回覆制數據
四.HotSpot虛擬機對象探秘
1、New對象過程
- new 指令發出
- 檢查 new 的參數是否在常量池中存在這個 Class 的符號引用
- 檢查對應的 Class 是否已經初始化
- 若沒有則先執行初始化過程
- 分配記憶體,檢查堆是否規整(垃圾收集器是否帶有壓縮整理功能決定)
- 規整:指針碰撞方式分配記憶體
- 不規整:空閑列表方式分配記憶體
- 記憶體空間初始化為零值(不包括對象頭)
- 對對象進行重要的配置
- 執行 < init > 方法
2、對象的記憶體佈局
對象頭(Mark Word)
- 自身運行時數據
- GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID
- 類型指針:確定對象是哪個Class的實例
實例數據
- 存儲有效信息,定義的各種欄位
- 相同寬度的欄位總是被分配到一起
對齊填充
- 不一定存在
- 實例數據沒有對齊,需要填充
3、對象的訪問定位
句柄(reference):
- 堆中劃分句柄池
- 句柄地址
- 到對象實例數據的地址
- 到對象類型的地址
- 優勢:穩定,對象移動時,(如GC時會移動),這個時候只改變指針地址。句柄信息不變,相對穩定;
指針:
- 直接存儲了上述的對象地址
- 優勢:速度快
五.OOM
- 堆溢出:舉例一直 new 新的實例對象
- 棧溢出:舉例無限迴圈調用執行某個方法
- 方法區和運行時常量池溢出:
- String.intern():如果常量池已存在,則返回 String 對象,如果不存在,則先添加到常量池,再返回 String 對象。
- 動態定義大量的 Class,需要註意記憶體的回收情況。