JVM記憶體模型,jvm模型

來源:互聯網
上載者:User

JVM記憶體模型,jvm模型

JVM定義了若干個程式執行期間使用的資料區域。這個地區裡的一些資料在JVM啟動的時候建立,在JVM退出的時候銷毀。而其他的資料依賴於每一個線程,線上程建立時建立,線上程退出時銷毀。


程式計數器

程式計數器是一塊較小的記憶體空間,可以看作是當前線程所執行的位元組碼的行號指標。分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。

由於JAVA 虛擬機器的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條線程中的指令。因此,為了線程切換後能恢複到正確的執行位置,每條線程都需要有一個獨立的程式計數器,各條線程之間的計數器互不影響,隔離儲存區 (Isolated Storage),我們稱這類記憶體地區為“線程私人”的記憶體。

如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空白(Undefined)。

此記憶體地區是唯一一個在JAVA 虛擬機器規範中沒有規定任何OutOfMemoryError情況的地區。


虛擬機器棧

線程私人,它的生命週期與線程相同。虛擬機器棧描述的是Java 方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存局部變數表、操作棧、動態連結、方法出口等資訊

動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機器的運行和動畫也類似,每個在虛擬機器中啟動並執行程式也是由許多的幀的切換產生的結果,只是這些幀裡面存放的是方法的局部變數,運算元棧,動態連結,方法返回地址和一些額外的附加資訊組成。每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

 

對於執行引擎來說,活動線程中,只有棧頂的棧幀是有效,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法執行引擎所啟動並執行所有位元組碼指令都只針對當前棧幀進行操作

局部變數表

局部變數表是一組變數值儲存空間,用於存放方法參數和方法內部定義的局部變數。在Java程式被編譯成Class檔案時,就在方法的Code屬性的max_locals資料項目中確定了該方法所需要分配的最大局部變數表的容量。

局部變數表的容量以變數槽(Slot)為最小單位,32位虛擬機器中一個Slot可以存放一個32位以內的資料類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。

reference類型虛擬機器規範沒有明確說明它的長度,但一般來說,虛擬機器實現至少都應當能從此引用中直接或者間接地尋找到對象在Java堆中的起始地址索引和方法區中的物件類型資料。

returnAddress類型是為位元組碼指令jsr、jsr_w和ret服務的,它指向了一條位元組碼指令的地址。

虛擬機器是使用局部變數表完成參數值到參數變數列表的傳遞過程的,如果是執行個體方法(非static),那麼局部變數表的第0位索引的Slot預設是用於傳遞方法所屬對象執行個體的引用,在方法中通過this訪問。

 Slot是可以重用的,當Slot中的變數超出了範圍,那麼下一次分配Slot的時候,將會覆蓋原來的資料。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。

 系統不會為局部變數賦予初始值(執行個體變數和類變數都會被賦予初始值)。也就是說不存在類變數那樣的準備階段。

運算元棧

和局部變數區一樣,運算元棧也是被組織成一個以字長為單位的數組。但是和前者不同的是,它不是通過索引來訪問,而是通過標準的棧操作——壓棧和出棧—來訪問的。比如,如果某個指令把一個值壓入到運算元棧中,稍後另一個指令就可以彈出這個值來使用。

虛擬機器在運算元棧中儲存資料的方式和在局部變數區中是一樣的:如int、long、float、double、reference和returnType的儲存。對於byte、short以及char類型的值在壓入到運算元棧之前,也會被轉換為int。

虛擬機器把運算元棧作為它的工作區——大多數指令都要從這裡彈出資料,執行運算,然後把結果壓回運算元棧。比如,iadd指令就要從運算元棧中彈出兩個整數,執行加法運算,其結果又壓回到運算元棧中,看看下面的樣本,它示範了虛擬機器是如何把兩個int類型的局部變數相加,再把結果儲存到第三個局部變數的:

beginiload_0    // push the int in local variable 0 ontothe stackiload_1    //push the int in local variable 1 onto the stackiadd       // pop two ints, add them, push resultistore_2   // pop int, store into local variable 2end
 

在這個位元組碼序列裡,前兩個指令iload_0和iload_1將儲存在局部變數中索引為0和1的整數壓入運算元棧中,其後iadd指令從運算元棧中彈出那兩個整數相加,再將結果壓入運算元棧。第四條指令istore_2則從運算元棧中彈出結果,並把它儲存到局部變數區索引為2的位置。詳細表述了這個過程中局部變數和運算元棧的狀態變化,圖中沒有使用的局部變數區和運算元棧地區以空白表示。

  

動態串連

虛擬機器啟動並執行時候,運行時常量池會儲存大量的符號引用,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調用代表棧幀B的方法,那麼這個虛擬機器的方法調用指令就會以B方法的符號引用作為參數,但是因為符號引用並不是直接指向代表B方法的記憶體位置,所以在調用之前還必須要將符號引用轉換為直接引用,然後通過直接引用才可以訪問到真正的方法。

如果符號引用是在類載入階段或者第一次使用的時候轉化為直接應用,那麼這種轉換成為靜態解析,如果是在運行期間轉換為直接引用,那麼這種轉換就成為動態串連。

返回地址

       方法的返回分為兩種情況,一種是正常退出,退出後會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法。

不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,如果方法是正常退出的,則調用者的PC計數器的值就可以作為返回地址,,果是因為異常退出的,則是需要通過異常處理表來確定。

方法的的一次調用就對應著棧幀在虛擬機器棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括:恢複上層方法的局部變數表以及運算元棧,如果有返回值的話,就把返回值壓入到調用者棧幀的運算元棧中,還會把PC計數器的值調整為方法調用入口的下一條指令。

  

異常

在JAVA 虛擬機器規範中,對虛擬機器棧規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError 異常;如果虛擬機器棧可以動態擴充(當前大部分的JAVA 虛擬機器都可動態擴充,只不過JAVA 虛擬機器規範中也允許固定長度的虛擬機器棧),當擴充時無法申請到足夠的記憶體時會拋出OutOfMemoryError 異常。


本地方法棧

本地方法棧(Native MethodStacks)與虛擬機器棧所發揮的作用是非常相似的,其區別不過是虛擬機器棧為虛擬機器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native 方法服務。虛擬機器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot 虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一

與虛擬機器棧一樣,本地方法棧地區也會拋出StackOverflowError和OutOfMemoryError異常。


堆是JAVA 虛擬機器所管理的記憶體中最大的一塊。Java 堆是被所有線程共用的一塊記憶體地區,在虛擬機器啟動時建立。此記憶體地區的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡分配記憶體。但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換最佳化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸層得不是那麼“絕對”了。

堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。

堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數設定,-Xms為JVM啟動時申請的最小記憶體,預設為作業系統實體記憶體的1/64但小於1G,-Xmx為JVM可申請的最大記憶體,預設為實體記憶體的1/4但小於1G,預設當空餘堆記憶體小於40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空餘堆記憶體大於70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對於運行系統,為避免在運行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。

 

如果從記憶體回收的角度看,由於現在收集器基本都是採用的分代收集演算法,所以Java 堆中還可以細分為:新生代和老年代;

新生代程式新建立的對象都是從新生代分配記憶體,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。

老年代:用於存放經過多次新生代GC仍然存活的對象,例如緩衝對象,建立的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啟動參數設定-XX:PretenureSizeThreshold=1024(單位為位元組,預設為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數組對象,且數組中無引用外部對象。

老年代所佔的記憶體大小為-Xmx對應的值減去-Xmn對應的值。

 

如果在堆中沒有記憶體完成執行個體分配,並且堆也無法再擴充時,將會拋出OutOfMemoryError 異常。


方法區

方法區在一個jvm執行個體的內部,類型資訊被儲存在一個稱為方法區的記憶體邏輯區中。類型資訊是由類載入器在類載入時從類檔案中提取出來的。類(靜態)變數也儲存在方法區中。

簡單說方法區用來儲存類型的中繼資料資訊,一個.class檔案是類被java虛擬機器使用之前的表現形式,一旦這個類要被使用,java虛擬機器就會對其進行裝載、串連(驗證、準備、解析)和初始化。而裝載(後的結果就是由.class檔案轉變為方法區中的一段特定的資料結構。這個資料結構會儲存如下資訊:

 

類型資訊

      這個類型的全限定名

      這個類型的直接超類的全限定名

      這個類型是類類型還是介面類型

      這個類型的存取修飾詞

      任何直接超介面的全限定名的有序列表

 

欄位資訊

      欄位名

      欄位類型

      欄位的修飾符

 

方法資訊

      方法名

      方法傳回型別

      方法參數的數量和類型(按照順序)

      方法的修飾符

 

其他資訊

      除了常量以外的所有類(靜態)變數

      一個指向ClassLoader的指標

      一個指向Class對象的指標

      常量池(常量資料以及對其他類型的符號引用)

 

JVM為每個已載入的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的資料項目象數組項一樣,是通過索引訪問的

 

每個類的這些中繼資料,無論是在構建這個類的執行個體還是調用這個類某個對象的方法,都會存取方法區的這些中繼資料。

構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來儲存當前對象執行個體屬性以及其父類的執行個體屬性(而這些屬性資訊都是從方法區獲得),注意,這裡並不是僅僅為當前對象的執行個體屬性分配空間,還需要給父類的執行個體屬性分配,到此其實我們就可以回答第一個問題了,即執行個體化父類的某個子類時,JVM也會同時構建父類的一個對象。從另外一個角度也可以印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味著執行個體的建立,所以子類執行個體化時,父類肯定也會被執行個體化。

類變數被類的所有執行個體共用,即使沒有類執行個體時你也可以訪問它。這些變數只與類相關,所以在方法區中,它們成為類資料在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區中為每個non-final類變數分配空間。

 

方法區主要有以下幾個特點:

1、方法區是安全執行緒的。由於所有的線程都共用方法區,所以,方法區裡的資料訪問必須被設計成安全執行緒的。例如,假如同時有兩個線程都企圖存取方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個線程去裝載它,而其它線程必須等待

2、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。

3、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集

 

可以通過-XX:PermSize-XX:MaxPermSize 參數限制方法區的大小。

對於習慣在HotSpot 虛擬機器上開發和部署程式的開發人員來說,很多人願意把方法區稱為“永久代”(PermanentGeneration),本質上兩者並不等價,僅僅是因為HotSpot 虛擬機器的設計團隊選擇把GC 分代收集擴充至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機器(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。

相對而言,垃圾收集行為在這個地區是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣“永久”存在了。這個地區的記憶體回收目標主要是針對常量池的回收和對類型的卸載。

當方法區無法滿足記憶體配置需求時,將拋出OutOfMemoryError異常。


總結

名稱

特徵

作用

配置參數

異常

程式計數器

佔用記憶體小,線程私人,

生命週期與線程相同

大致為位元組碼行號指標

虛擬機器棧

線程私人,生命週期與線程相同,使用連續的記憶體空間

Java 方法執行的記憶體模型,儲存局部變數表、操作棧、動態連結、方法出口等資訊

-Xss

StackOverflowError

OutOfMemoryError

java堆

線程共用,生命週期與虛擬機器相同,可以不使用連續的記憶體位址

儲存對象執行個體,所有對象執行個體(包括數組)都要在堆上分配

-Xms

-Xsx

-Xmn

OutOfMemoryError

方法區

線程共用,生命週期與虛擬機器相同,可以不使用連續的記憶體位址

儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料

-XX:PermSize:

16M

-XX:MaxPermSize

64M

OutOfMemoryError

運行時常量池

方法區的一部分,具有動態性

存放字面量及符號引用

 

 


直接記憶體

直接記憶體(Direct Memory)並不是虛擬機器運行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體地區,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現,所以我們放到這裡一起講解。

在JDK 1.4 中新加入了NIO(NewInput/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O 方式,它可以使用Native 函數庫直接分配堆外記憶體,然後通過一個儲存在Java 堆裡面的DirectByteBuffer 對象作為這塊記憶體的引用進行操作。這樣能在一些情境中顯著提高效能,因為避免了在Java 堆和Native 堆中來回複製資料。


堆與棧的對比

經常有人把Java 記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),這種分法比較粗糙,Java記憶體地區的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程式員最關注的、與對象記憶體配置關係最密切的記憶體地區是這兩塊。

堆很靈活,但是不安全。對於對象,我們要動態地建立、銷毀,不能說後建立的對象沒有銷毀,先前建立的對象就不能銷毀,那樣的話我們的程式就寸步難行,所以Java中用堆來儲存物件。而一旦堆中的對象被銷毀,我們繼續引用這個對象的話,就會出現著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時才會被發現。

棧不靈活,但是很嚴格,是安全的,易於管理。因為只要上面的引用沒有銷毀,下面引用就一定還在,在大部分程式中,都是先定義的變數、引用先進棧,後定義的後進棧,同時,區塊內部的變數、引用在進入區塊時壓棧,區塊結束時出棧,理解了這種機制,我們就可以很方便地理解各種程式設計語言的範圍的概念了,同時這也是棧的優點——錯誤的引用邏輯在編譯時間就可以被發現。

棧--主要存放引用和基礎資料型別 (Elementary Data Type)。

堆--用來存放 new 出來的對象執行個體。


記憶體溢出和記憶體流失

記憶體溢出 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢出。

記憶體泄露 memory leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體泄露危害可以忽略,但記憶體泄露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。

memory leak會最終會導致out ofmemory。

 

Java 堆記憶體的OutOfMemoryError異常是實際應用中最常見的記憶體溢出異常情況。出現Java 堆記憶體溢出時,異常堆棧資訊“java.lang.OutOfMemoryError”會跟著進一步提示“Java heapspace”。

要解決這個地區的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認記憶體中的對象是否是必要的,也就是要先分清楚到底是出現了記憶體流失(Memory Leak)還是記憶體溢出(Memory Overflow)。

如果是記憶體流失,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈。於是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關聯並導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型資訊,以及GC Roots 引用鏈的資訊,就可以比較準確地定位出泄漏代碼的位置。

如果不存在泄漏,換句話說就是記憶體中的對象確實都還必須存活著,那就應當檢查虛擬機器的堆參數(-Xmx 與-Xms),與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程式運行期的記憶體消耗。


記憶體配置過程

1、JVM 會試圖為相關Java對象在Eden Space中初始化一塊記憶體地區。

2、當Eden空間足夠時,記憶體申請結束;否則到下一步。

3、JVM 試圖釋放在Eden中所有不活躍的對象(這屬於1或更進階的記憶體回收)。釋放後若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。

4、Survivor區被用來作為Eden及Old的中間交換地區,當Old區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區。

5、當Old區空間不夠時,JVM 會在Old區進行完全的垃圾收集(0級)。

6、完全垃圾收集後,若Survivor及Old區仍然無法存放從Eden複製過來的部分對象,導致JVM無法在Eden區為新對象建立記憶體地區,則出現“outofmemory”錯誤。


對象訪問

對象訪問在Java 語言中無處不在,是最普通的程式行為,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要記憶體地區之間的關聯關係,如下面的這句代碼:

 

Object obj = newObject();

 

假設這句代碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的本地變數表中,作為一個reference 類型資料出現。而“new Object()”這部分的語義將會反映到Java 堆中,形成一Block Storage了Object 類型所有執行個體資料值(Instance Data,對象中各個執行個體欄位的資料)的結構化記憶體,根據具體類型以及虛擬機器實現的對象記憶體布局(Object Memory Layout)的不同,這塊記憶體的長度是不固定的。另外,在Java 堆中還必須包含能尋找到此物件類型資料(如物件類型、父類、實現的介面、方法等)的地址資訊,這些類型資料則儲存在方法區中。

由於reference 類型在JAVA 虛擬機器規範裡面只規定了一個指向對象的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機器實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼直接指標

如果使用控制代碼訪問方式,Java 堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是對象的控制代碼地址,而控制代碼中包含了對象執行個體資料和類型資料各自的具體地址資訊。

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.