注意:本系列部落格,主要參考自以下四本書
《分散式Java應用:基礎與實踐》《深入理解Java虛擬機器(第二版)》《深入分析Java web技術內幕》《實戰java虛擬機器》
1、為什麼要了解JVM記憶體管理機制
- JVM自動的管理記憶體的分配與回收,這會在不知不覺中浪費很多記憶體,導致JVM花費很多時間去進行垃圾回收(GC)
- 記憶體洩露,導致JVM記憶體最終不夠用
2、JVM記憶體結構
根據上圖,JVM記憶體結構包括:
- 方法區(也就是"持久代"),java8裡徹底被移除,取而代之的是後設資料區
- 堆
- 棧(在hotspot JVM中,JVM方法棧--Java虛擬棧,與本地方法棧是同一個)
- PC暫存器(程式計數器)
還有一塊:
- 直接記憶體:直接向系統記憶體申請的一塊記憶體區域,javaNIO會使用,速度優於java堆記憶體。- 隸屬於實體記憶體,不屬於JVM記憶體
注意點:
- 堆是GC的主要區域,方法區、直接記憶體也會發生GC
- 棧與PC暫存器是每個執行緒都會建立的私有區域,不會GC
- 直接記憶體使用速度由於堆記憶體,但是記憶體的申請速度低於堆記憶體
2.1、方法區
- 存放內容(類的資訊、類static屬性、方法、常量池)
- 已經載入的類的資訊(名稱、修飾符等)
- 類中的static變數
- 類中的field資訊
- 類中定義為final常量
- 類中的方法資訊
- 執行時常量池:編譯器生成的各種字面量和符號引用(編譯期)儲存在class檔案的常量池中,這部分內容會在類載入之後進入執行時常量池,class檔案的常量池檢視 第三章 類檔案結構與javap的使用
- 使用例項:反射,在程式中通過Class物件呼叫getName等方法獲取資訊資料時,這些資訊資料來源於方法區。
- 調節引數
- -XX:PermSize:指定方法區的最小值,預設為16M
- -XX:MaxPermSize:指定方法區的最大值,預設為64M
- 所拋錯誤
- 方法區域要使用的記憶體超過了其允許的大小時,丟擲OutOfMemoryError
- 記憶體回收的主要目標
- 對類的解除安裝(這也是為什麼很多企業使用velocity等模板引擎做前端而不是使用jsp的原因之一)
- 針對常量池的回收
- 總結
- 一般而言,在企業開發中,-XX:PermSize==-XX:MaxPermSize
- 通常,這個大小設定為256M就沒問題了,當然還要根據自己的程式去預估,並在執行過程中去調整,這裡以在Resin伺服器中配置為例
<jvm-arg>-XX:PermSize=256M</jvm-arg> <jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
- 類中的static變數會在方法區分配記憶體,但是類中的例項變數不會(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧)
- 關於方法區的存放內容,可以這樣去想所有的通過Class物件可以反射獲取的都是從方法區獲取的(包括Class物件也是方法區的,Class是該類下所有其他資訊的訪問入口)
注意:常量池在jdk1.6在方法區;在jdk1.7在堆
附:後設資料區
- 調節引數:-XX:MaxMetaspaceSize,如果不指定大小,極限情況下可能耗盡系統所有記憶體
- 後設資料區是堆外的一塊直接記憶體
2.2、堆
- 存放內容
- 物件例項(類中的例項變數會隨著物件例項的建立一起分配在堆中,當然若是基本資料型別的話,會隨著物件的建立直接壓入運算元棧),這一點檢視 第四章 類載入機制
- 陣列值
- 使用例項
- 所有通過new建立的物件都在這塊兒記憶體分配,具體分配到年輕代還是年老代需要根據配置引數而定(新建物件直接分配到年老代有兩種情況,看下邊)
- 調節引數
- -Xmx:最大堆記憶體,預設為實體記憶體的1/4但小於1G
- -Xms:最小堆記憶體,預設為實體記憶體的1/64但小於1G
- -XX:MinHeapFreeRatio,預設當空餘堆記憶體小於最小堆記憶體的40%時,堆記憶體增大到-Xmx
- -XX:MaxHeapFreeRatio,當空餘堆記憶體大於最大堆記憶體的70%時,堆記憶體減小到-Xms
- 注意點
- 在實際使用中,-Xmx與-Xms配置成相等的,這樣,堆記憶體就不會頻繁的進行調整了
- 丟擲錯誤
- OutOfMemoryError:在堆中沒有記憶體完成例項分配(關於例項記憶體的分配,之後再說),此時堆記憶體已達到最大無法擴充套件時。
- 堆記憶體劃分
-
- 新生代
- 組成:Eden+From(S0)+To(S1)
- -Xmn:整個新生代的大小
- -XX:SurvivorRatio:調整Eden:From(To)的比率,預設為8:1
- 年老代
- 新建物件直接分配到年老代,兩種情況
- 大物件:-XX:PretenureSizeThreshold(單位:位元組)引數來指定大物件的標準,在Parallel Scavenge GC下可能無效,具體見《第五章 JVM垃圾收集器(1) 》
- 大陣列:陣列中的元素沒有引用任何外部的物件
- 新建物件直接分配到年老代,兩種情況
- 新生代
- 總結
- 企業開發中,-Xmx==-Xms
- 通常,-Xmx設定為2048m就沒問題了,當然還要根據自己的程式去預估,並在執行過程中去調整,這裡以在Resin伺服器中配置為例
<jvm-arg>-Xms2048m</jvm-arg> <jvm-arg>-Xmx2048m</jvm-arg> <jvm-arg>-Xmn512m</jvm-arg> <jvm-arg>-XX:SurvivorRatio=8</jvm-arg> <jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年輕代大小-Xmn==512m,這樣,年老代大小就是2048-512==1536m,這個比率值得記住,在企業開發中,年輕代:年老代==1:3,而此時,我們配置的-XX:MaxTenuringThreshold=15(這也是預設值),年輕代物件經過15次的複製後進入到年老代(關於這一點,在之後的GC機制中會說),
- -XX:MaxTenuringThreshold與-XX:PretenureSizeThreshold不一樣,不要看錯
2.3、棧
- 注意點
- 每條執行緒都會分配一個棧,每個棧中有多個棧幀(每一個方法對應一個棧幀)
- 執行緒建立的時候建立一個執行緒的java棧
- 每個方法在執行的同時都會建立一個棧幀,每個棧幀用於儲存當前方法的區域性變數表、運算元棧等,具體檢視本文第一個圖,每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程,說的更明白一點,就是方法執行時建立棧幀,方法結束時釋放棧幀所佔記憶體
- 存放內容
- 區域性變數表:八大基本資料型別資料、物件引用。該空間在編譯期已經分配好,執行期不變。
- 運算元棧:是執行引擎直接操作的部分
- 調節引數
- -Xss:設定棧的大小,通常設定為1m就好
<jvm-arg>-Xss1m</jvm-arg>
- -Xss:設定棧的大小,通常設定為1m就好
- 支援native方法執行(本地方法棧)
- 所拋錯誤
- StackOverFlowError:執行緒請求的棧深度大於虛擬機器所允許的深度。
- 棧的深度就是方法呼叫巢狀的層數,受限於-Xss的大小
- 典型場景:沒有終止條件的遞迴(遞迴基於棧)。
- 每個方法的棧的深度在javac編譯之後就已經確定了,檢視 第三章 類檔案結構與javap的使用
- OutOfMemoryError:虛擬機器棧可以動態擴充套件,如果擴充套件的時候無法申請到足夠的記憶體。
- 需要注意的是,棧可以動態擴充套件,但是棧中的區域性變數表不可以。
- StackOverFlowError:執行緒請求的棧深度大於虛擬機器所允許的深度。
2.4、PC暫存器(程式計數器)
- 概念:當前執行緒所執行的位元組碼的行號指示器,用於位元組碼直譯器對位元組碼指令的執行。
- 多執行緒:通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條執行緒中的指令,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。
附:物件分配(《實戰java虛擬機器》)