JVM記憶體分析

feri發表於2018-05-27

JVM記憶體模型分析
JVM會將Java程式所管理的記憶體劃分為若干不同的資料區域. 這些區域有各自的用途、建立/銷燬時間

JVM記憶體資料:棧管執行,堆管儲存
第一章 執行緒私有區域
執行緒私有資料區域生命週期與執行緒相同, 依賴使用者執行緒的啟動/結束而建立/銷燬(在Hotspot VM內, 每個執行緒都與作業系統的本地執行緒直接對映, 因此這部分記憶體區域的存/否跟隨本地執行緒的生/死)
1.1 Native Method Stack本地方法棧
是在Native Method Stack中登記native方法,在Execution Engine執行時載入native libraies。
本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native方法服務。虛擬機器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError和OutOfMemoryError異常。
1.2 PC Register程式計數器
每個執行緒都有一個程式計算器,就是一個指標,指向方法區中的方法位元組碼(下一個將要執行的指令程式碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。
作用是當前執行緒所執行位元組碼的行號指示器(類似於傳統CPU模型中的PC), PC在每次指令執行後自增, 維護下一個將要執行指令的地址. 在JVM模型中, 位元組碼直譯器就是通過改變PC值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴PC完成(僅限於Java方法, Native方法該計數器值為undefined).
不同於OS以程式為單位排程, JVM中的併發是通過執行緒切換並分配時間片執行來實現的. 在任何一個時刻, 一個處理器核心只會執行一條執行緒中的指令. 因此, 為了執行緒切換後能恢復到正確的執行位置, 每條執行緒都需要有一個獨立的程式計數器, 這類記憶體被稱為“執行緒私有”記憶體.
1.3 Java Stack(虛擬機器棧)
棧也叫棧記憶體,主管Java程式的執行,是線上程建立時建立,它的生命期是跟隨執行緒的生命期,執行緒結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要執行緒一結束該棧就Over,生命週期和執行緒一致,是執行緒私有的。
  基本型別的變數和物件的引用變數都是在函式的棧記憶體中分配。
棧幀中主要儲存3類資料:
   本地變數(Local Variables):輸入引數和輸出引數以及方法內的變數;
   棧操作(Operand Stack):記錄出棧、入棧的操作;
   棧幀資料(Frame Data):包括類檔案、方法等等。
棧執行原理
   棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法和執行期資料的資料集,當一個方法A被呼叫時就產生了一個棧幀F1,並被壓入到棧中,A方法又呼叫了B方法,於是產生棧幀F2也被壓入棧,B方法又呼叫了C方法,於是產生棧幀F3也被壓入棧…… 依次執行完畢後,先彈出後進……F3棧幀,再彈出F2棧幀,再彈出F1棧幀。
遵循“先進後出”/“後進先出”原則。
第二章 執行緒共享區域
隨虛擬機器的啟動/關閉而建立/銷燬
2.1 Method Area方法區
  常說的永久代(Permanent Generation), 用於儲存被JVM載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料. HotSpot VM把GC分代收集擴充套件至方法區, 即使用Java堆的永久代來實現方法區, 這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體, 而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目標是針對常量池的回收和型別的解除安裝, 因此收益一般很小)。
1.7和1.8的異同
不過在1.7的HotSpot已經將原本放在永久代的字串常量池移出
而在1.8中, 永久區已經被徹底移除, 取而代之的是後設資料區Metaspace(這一點在檢視GC日誌和使用jstat -gcutil檢視GC情況時可以觀察到),與永久代不同, 如果不指定Metaspace大小, 如果方法區持續增長, VM會預設耗盡所有系統記憶體.
執行時常量池
方法區的一部分. Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項常量池(Constant Pool Table)用於存放編譯期生成的各種字面量和符號引用, 這部分內容會存放到方法區的執行時常量池中(如前面從test方法中讀到的signature資訊). 但Java語言並不要求常量一定只能在編譯期產生, 即並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池, 執行期間也可能將新的常量放入池中, 如String的intern()方法.
2.2 Heap(Java堆)
  堆這塊區域是JVM中最大的,應用的物件和資料都是存在這個區域,這塊區域也是執行緒共享的,也是 gc 主要的回收區,一個 JVM 例項只存在一個堆類存,堆記憶體的大小是可以調節的。類載入器讀取了類檔案後,需要把類、方法、常變數放到堆記憶體中,以方便執行器執行,堆記憶體分為三部分:

新生區
  新生區是類的誕生、成長、消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分:伊甸區(Eden space)和倖存者區(Survivor pace),所有的類都是在伊甸區被new出來的。倖存區有兩個:0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園進行垃圾回收(Minor GC),將伊甸園中的剩餘物件移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1去也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生Major GC(FullGCC),進行養老區的記憶體清理。若養老區執行Full GC 之後發現依然無法進行物件的儲存,就會產生OOM異常“OutOfMemoryError”。
  如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機器的堆記憶體不夠。原因有二:
    a.Java虛擬機器的堆記憶體設定不夠,可以通過引數-Xms、-Xmx來調整。
b.程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)。
養老區
  養老區用於儲存從新生區篩選出來的 JAVA 物件,一般池物件都在這個區域活躍。
永久區
  永久儲存區是一個常駐記憶體區域,用於存放JDK自身所攜帶的 Class,Interface 的後設資料,也就是說它儲存的是執行環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所佔用的記憶體。
  如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機器對永久代Perm記憶體設定不夠。原因有二:
     a. 程式啟動需要載入大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。
     b. 大量動態反射生成的類不斷被載入,最終導致Perm區被佔滿。
說明:
Jdk1.6及之前:常量池分配在永久代 。
Jdk1.7:有,但已經逐步“去永久代” 。
Jdk1.8及之後:無(java.lang.OutOfMemoryError: PermGen space,這種錯誤將不會出現在JDK1.8中)。
說明:方法區和堆記憶體的異議:
 實際而言,方法區和堆一樣,是各個執行緒共享的記憶體區域,它用於儲存虛擬機器載入的:類資訊+普通常量+靜態常量+編譯器編譯後的程式碼等等,雖然JVM規範將方法區描述為堆的一個邏輯部分,但它卻還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。
     對於HotSpot虛擬機器,很多開發者習慣將方法區稱之為“永久代(Parmanent Gen)”,但嚴格本質上說兩者不同,或者說使用永久代來實現方法區而已,永久代是方法區的一個實現,jdk1.7的版本中,已經將原本放在永久代的字串常量池移走。

相關文章