第一章 JVM記憶體結構

weixin_34126215發表於2016-02-03

注意:本系列部落格,主要參考自以下四本書

《分散式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>
      View Code
    •  類中的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>
      View Code

      可以看到,-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>
      View Code
  • 支援native方法執行(本地方法棧)
  • 所拋錯誤
    • StackOverFlowError:執行緒請求的棧深度大於虛擬機器所允許的深度。
      • 棧的深度就是方法呼叫巢狀的層數,受限於-Xss的大小
      • 典型場景:沒有終止條件的遞迴(遞迴基於棧)。
      • 每個方法的棧的深度在javac編譯之後就已經確定了,檢視 第三章 類檔案結構與javap的使用 
    • OutOfMemoryError:虛擬機器棧可以動態擴充套件,如果擴充套件的時候無法申請到足夠的記憶體。
      • 需要注意的是,棧可以動態擴充套件,但是棧中的區域性變數表不可以

 

2.4、PC暫存器(程式計數器)

  • 概念:當前執行緒所執行的位元組碼的行號指示器,用於位元組碼直譯器對位元組碼指令的執行。
  • 多執行緒:通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條執行緒中的指令,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存。

附:物件分配(《實戰java虛擬機器》)

 

相關文章