天天看點

學習JVM-運作時資料區

 之前的關于OOM異常的學習筆記,強迫自己整理了異常及對應的參數。本片學習筆記梳理下相關背景知識。 一背景: 作為java碼農,對于常見的編碼,編譯,執行比較熟悉了。更加關注架構跟業務實作,但是回頭想想,當我們執行java指令後究竟發生了什麼,就是我們通過JVM與機器互動,Java通過使用Java虛拟機屏蔽了與具體平台相關的資訊,使得Java具備了一次編寫,多處運作的特性。

學習JVM-運作時資料區

JVM主要由 類加載器子系統、運作時資料區(記憶體空間)、執行引擎以及與本地方法接口 等組成。 由此可以看出,類加載器把.class檔案加載入記憶體,供以後執行個體化。此處與後面的記憶體區域有關。記憶體空間又跟java記憶體模型有關,更是牽扯出垃圾回收,OOM異常,記憶體參數優化等。 執行引擎牽扯到指令優化就是JMM相關。是以jvm既是難點又是重點,了解了這些,才會有豁然開朗的感覺。當然我目前還在學習中,好多不了解的。比如沿着一個思路,同步背後的鎖,再底層的aqs,在底層的CAS,再底層的CPU實作機制。可以一層層的熟悉,跟作業系統有關的了。好了,扯遠了。 二 JVM運作時資料區

 好了,現在看看筆記整理的重點。

學習JVM-運作時資料區

上圖分為兩個部分:線程私有區(pc計數器、棧、本地方法棧 ),線程共享區(方法區、堆)。下面分别介紹。   2.1程式計數器(The pc Register) JVM一次能支援很多線程執行。每一個JVM線程有它自己的程式計數器。PC寄存器裡儲存有目前正在執行的JVM指令的位址。注意:( 一個JVM的線程都正在執行目前線程的方法代碼。如果這個方法不是本地( native )方法,程式計數器包含目前被執行的JVM位址。如果線程正在執行本地( native )方法,程式計數器的值為未定義。)上面這句話隻是給出結果,不是很了解。

2.2棧(JVM Stacks)

每個JVM的線程在建立的時候,都會建立一個棧。一個棧包含很多棧桢。JVM的棧好比傳統語言C的棧,它維持(存儲)本地變量和部分結果,并在方法調用和傳回中(被)使用。這個棧是一個後進先出的資料結構,是以目前正在執行的方法在棧的頂端,每當一個方法被調用時,一個新的棧幀就會被建立然後放在了棧的頂端。當方法正常傳回或者發生了未捕獲的異常,棧幀就會從棧裡移除。

2.2.1 棧幀(stack frame)

JVM為每個方法調用建立一個新的棧幀并推到每個方法調用的棧頂。當方法正常傳回或者遇到了未捕獲的異常,這個棧幀将被移除。

每個棧幀包含了:局部變量表、傳回值、操作數棧、目前方法所在的類的運作時常量池引用。見下圖

學習JVM-運作時資料區

 2.2.2 局部變量表(Local variable array)

局部變量表包含了這個方法執行期間所有用到的變量,包括this引用,所有方法參數以及其他的局部聲明變量。對于類方法(比如靜态方法)來說,所有方法參數下标都是從0開始,然而,對于執行個體方法來說這個0是留給this的。對于對象,局部變量區中永遠隻有指向堆的引用。

2.2.3操作數棧(Operand stack)

方法實際運作的工作空間。每個方法都在操作數棧和局部變量數組之間交換資料,并且壓入或者彈出其他方法傳回的結果。

2.2.3動态連結(幀資料)

每個棧幀都包含了運作時常量池的引用。這個引用指向了這個棧幀正在執行的方法所在的類的常量池,它對動态連結提供了支援。

局部變量區和操作數棧的大小依照具體方法在編譯時就已經确定。調用方法時會從方法區中找到對應類的類型資訊,從中得到具體方法的局部變量區和操作數棧的大小,依此配置設定棧幀記憶體,壓入Java棧。

棧基本概念結束了,可以結合之前總結OOM異常,

 JVM說明書(規範)允許棧要麼是一個固定大小,要麼動态擴充來滿足計算的要求。如果JVM棧是一個固定的大小,當棧被建立的時候每一個棧大小可以自由設定。 在動态擴充情況下,可以控制最大最小記憶體。 在VM Spec中對這個區域規定了2種異常狀況(以下兩種異常與JVM的棧機制有關):

如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常。

如果VM棧可以動态擴充,在初始化新線程時沒有足夠記憶體建立棧則抛出OutOfMemoryError異常。

棧容量隻由-Xss參數設定

2.3本地方法棧(Native Method Stacks) 通俗說就是供用非Java語言實作的本地方法的堆棧 。 并不是所有的JVM都支援本地方法。不過那些支援的通常會建立出每個線程的本地方法 棧。如果提供本地方法棧,每個線程建立時必須配置設定一個本地方法棧。是不是可以了解為java調用了C代碼,就建立一個C棧?待确認哈。

2.4堆(heap) J VM有一個在所有線程内共享的堆。 堆在虛拟機啟動的時候建立, 堆是給所有類的執行個體和數組配置設定記憶體的運作時資料區。 數組和對象可能永遠不會存儲在棧上,因為一個棧幀并不是設計為在建立後會随時改變大小。棧幀僅僅儲存引用,這個引用指向對象或者數組在堆中的位置。與局部變量表(每個棧幀裡)中的基本資料類型和引用不同,對象總是被存儲在堆裡,是以他們在方法結束後不會被移除,僅僅在垃圾收集的時候才會被移除。堆中儲存的對象通過一個自動存儲管理系統(垃圾回收器)進行回收。 對象從不明确的被配置設定(JVM從不指明對象的釋放)。 插一句: 堆是所有線程共享的,是以在進行執行個體化對象等操作時,需要解決同步問題。 為了提高GC效率,從jd k1.2開始将堆記憶體做分代(Generation)處理。

通常他們的工作流程如下:

新對象和數組被配置設定在年輕代。

年輕代會發生Minor GC。 對象如果仍然存活,将會從eden區移到survivor區。

Major GC 通常會導緻應用線程暫停,它會在2個區中移動對象,如果對象依然存活,将會從年輕代移到老年代。

當每次老年代進行垃圾收集的時候,會觸發持久代帶也進行一次收集。同樣,在發生full gc的時候他們2個也會被收集一次。

2.4.1年輕代

 HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分别叫from和to)。預設比例為8:1,因為年輕代中的對象基本都是朝生夕死的(80%以上),是以在年輕代的垃圾回收算法使用的是複制算法。

年輕代有關參數。

1)-XX:NewSize和-XX:MaxNewSize

用于設定年輕代的大小,建議設為整個堆大小的1/3或者1/4,兩個值設為一樣大。

2)-XX:SurvivorRatio

用于設定Eden和其中一個Survivor的比值,這個值也比較重要。

3)-XX:+PrintTenuringDistribution

這個參數用于顯示每次Minor GC時Survivor區中各個年齡段的對象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于設定晉升到老年代的對象年齡的最小值和最大值,每個對象在堅持過一次Minor GC之後,年齡就加1。

2.4.2 老年代

存放那些在曆經了Eden區和Survivor區的多次GC後仍然存活下來的對象。

這裡有個對象配置設定的優化,可以結合OOM異常的demo來看。

對于小對象的配置設定,會優先線上程私有的 TLAB (Thread Local Allocation Buffer)中配置設定(因為在堆上配置設定記憶體需要鎖定整個堆,而在TLAB上則不需要,JVM在配置設定對象時會盡量在TLAB上配置設定,以提高效率),TLAB中建立的對象,不存在鎖甚至是CAS的開銷。TLAB占用的空間在Eden Generation。

當對象比較大,TLAB的空間不足以放下,而JVM又認為目前線程占用的TLAB剩餘空間還足夠時,就會直接在Eden Generation上配置設定,此時是存在并發競争的,是以會有CAS的開銷,但也還好。當對象大到Eden Generation放不下時,JVM隻能嘗試去Old Generation配置設定,這種情況需要盡可能避免,因為一旦在Old Generation配置設定,這個對象就隻能被Old Generation的GC或是FullGC回收了。

堆有關參數:-Xms -Xmx -Xmn 其中:old=Xmx-Xmn

2.5方法區(Method Area)

也被稱為非堆區域(在HotSpot JVM的實作當中)

JVM的方法區是所有線程共享的,方法區類似于傳統語言編譯代碼時的存儲區域或類似于作業系統程序的文本段。他存儲内容包括:每一個類的結構,如運作時常量池,字段和方法的資料;方法和構造器的代碼,如用于類,執行個體和接口初始化的特殊方法。這個方法區在JVM啟動的時候被建立,一般情況下JVM不會選擇對方法區進行垃圾回收或者壓縮.在HotSpot JVM裡,方法區被稱為永久區或者永久代(PermGen)。

所有線程都共享同樣的方法區,是以通路方法區的資料和動态連結的過程都是線程安全的。如果兩個線程嘗試通路一個類的字段或者方法而這個類還沒有加載,這個類就一定會首先被加載而且僅僅加載一次,這2個線程也一定要等到加載完後才會繼續執行。

2.5.4 運作時常量池(Runtime Constant Pool)

運作時常量池是類和接口運作時的常量池表,它在位元組碼檔案裡。它包含幾類常量。 在編譯時期識别的數值常量,在運作區識别的方法或引用字段。運作區常量池類似于傳統語言的字元表,但它比傳統字元表所存儲的範圍更廣。每一個運作區常量池從方法區配置設定記憶體。當類和接口被JVM建立時相應的常量池也被建立。換句話說:當一個方法或者變量被引用時,JVM通過運作時常量區來查找方法或者變量在記憶體裡的實際位址。

幾種在常量池記憶體儲的資料類型包括:

數量值、字元串值、類引用、字段引用、方法引用

它可以通過-XX:PermSize及-XX:MaxPermSize來進行調節。可以結合之前OOM異常學習筆記來看。

運作區常量池包括以下異常:

  • 當類和接口建立時,如果運作區常量池所需記憶體不足,則抛出OutOfMemoryError。

拓展知識點:

1.從jdk8開始。持久代已經被元空間(Metadata )取代。

它是本地堆記憶體中的一部分

它可以通過-XX:MetaspaceSize和-XX:MaxMetaspaceSize來進行調整

當到達XX:MetaspaceSize所指定的門檻值後會開始進行清理該區域

如果本地空間的記憶體用盡了會收到java.lang.OutOfMemoryError: Metadata space的錯誤資訊。

和持久代相關的JVM參數-XX:PermSize及-XX:MaxPermSize将會被忽略掉。

2. 代碼緩存——在JVM裡,Java位元組碼被解釋運作,但是它沒有直接運作本地代碼快。為了提高性能,Oracle Hotspot VM會尋找位元組碼的”熱點”區域,它指頻繁被執行的代碼,然後編譯成本地代碼。這些本地代碼會被儲存在堆外記憶體的代碼緩存區。Hotspot用這種方式,盡力去選擇最合适的方式來權衡編譯本地代碼的時間和直接解釋執行代碼的時間。編譯後的代碼就是本地代碼(硬體相關的),它是由JIT(Just In Time)編譯器生成的,這個編譯器是HotSpot JVM所特有的。

*********************************總結******************************************** JVM真是博大精深啊,本次學習筆記屬于OOM引發的JVM記憶體背景梳理。 與之相關的有GC,參數優化,并發與記憶體模型,甚至還有String這樣神奇的對象,一篇寫不完,分開整理。就到這裡吧

參考并發程式設計網: http://ifeve.com/jvm-runtime-data/ http://ifeve.com/jvm-yong-generation/ http://ifeve.com/jvm-permgen-where-art-thou/ http://ifeve.com/jvm-internals/ http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html