咱們在開發 Java 程序的過程基本不用關心 Java 運行時的內存管理,是由於 Java 程序在運行時內存都由虛擬機來進行管理。Java 虛擬機在執行 Java 程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域,咱們稱之爲運行時數據區域
。java
根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域。spring
咱們能夠經過不一樣的幾個維度對上圖稍做一下分析:數據結構
程序計數器是一塊較小的內存空間是,它能夠看作是當前線程所執行的字節碼的行號指示器。是線程私有的,每條線程都會有一個獨立的程序計數器。是爲了在多線程狀況下,線程切換後可以恢復到正確的執行位置。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。多線程
程序計數器在線程執行不一樣方法時儲存的內容會有所不一樣:若是線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼的內存地址;若是正在執行的是一個 native 方法,這個計數器值則爲未定義(Undefined)。jvm
Q:Java多線程執行native方法時程序計數器爲空,那麼線程切換後如何找到以前執行到哪裏了?ide
A: 對native方法而言,它的方法體並非由Java字節碼構成的,天然沒法應用上述的「Java字節碼地址」的概念。因此JVM規範規定,若是當前執行的方法是native的,那麼pc寄存器的值未定義——是什麼值均可以。Java線程老是須要以某種形式映射到OS線程上。映射模型能夠是1:1(原生線程模型)、n:1(綠色線程 / 用戶態線程模型)、m:n(混合模型)。post
以HotSpot VM的實現爲例,它目前在大多數平臺上都使用1:1模型,也就是每一個Java線程都直接映射到一個OS線程上執行。此時,native方法就由原平生臺直接執行,並不須要理會抽象的JVM層面上的「pc寄存器」概念——原生的CPU上真正的PC寄存器是怎樣就是怎樣。就像一個用C或C++寫的多線程程序,它在線程切換的時候是怎樣的,Java的native方法也就是怎樣的。 -Java線程老是須要以某種形式映射到OS線程上。映射模型能夠是1:1(原生線程模型)、n:1(綠色線程 / 用戶態線程模型)、m:n(混合模型)。性能
以HotSpot VM的實現爲例,它目前在大多數平臺上都使用1:1模型,也就是每一個Java線程都直接映射到一個OS線程上執行。此時,native方法就由原平生臺直接執行,並不須要理會抽象的JVM層面上的「pc寄存器」概念——原生的CPU上真正的PC寄存器是怎樣就是怎樣。就像一個用C或C++寫的多線程程序,它在線程切換的時候是怎樣的,Java的native方法也就是怎樣的。 - RednaxelaFXthis
本地方法棧是爲虛擬機使用到的 Native 方法服務。關於 Native 方法,官方給的說明是""。即在虛擬機規範中對本地方法棧中方法使用的語言、使用方式與數據結構並無強制規定,所以具體的虛擬機能夠自由實現它。本地方法棧區域會拋出StackOverflowError
和OutOfMemoryError
異常spa
Native Method:
A native method is a Java method whose implementation is provided by non-java code.
public static native double getDouble(Object array, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException;這些方法的聲明描述了一些非java代碼在這些java代碼裏看起來像什麼樣子。
Java 虛擬機棧是線程私有的,描述的是 Java 方法執行過程的內存模型:每一個方法在執行同時都會建立一個棧幀。每一個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。棧幀中儲存局部變量表、操做數棧、動態連接、方法出口等信息。
是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。存放了編譯時期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)和對象的引用。局部變量表在編譯階段就肯定了須要分配的最大容量。
局部變量表的容量以變量槽(Variable Slot)爲最小單位。每一個 Slot 都應該能存放一個 boolean、byte、char、short、int、float、reference 或 returnAddress 類型的數據。
虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從 0 開始至局部變量表最大的 Slot 數量。
下面咱們看一個具體的代碼示例:
public int calc() { int a = 100; int b = 200; int c = 300; return (a + b) * c; }
上述代碼反編譯後:
public int calc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=1 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 12: iload_2 13: iadd 14: iload_3 15: imul 16: ireturn LineNumberTable: line 10: 0 line 11: 3 line 12: 7 line 13: 11 LocalVariableTable: Start Length Slot Name Signature 0 17 0 this Lai/advance/common/VariableLocal; 3 14 1 a I 7 10 2 b I 11 6 3 c I
咱們注意到反編譯後的代碼第五行locals=4
就說明了咱們咱們局部變量表的大小爲 4 個 Slot,最下面的LocalVariableTable
展現了局部變量表裏面具體的內容。對於實例方法(非 static)局部變量表第 0 位索引的 Slot 默認值實例的引用,也就是咱們一直使用的this
關鍵,其他局部變量依次排序。
操做數棧也稱操做棧,它是一個後入先出(Last In First Out,LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯時期寫入到 Code
屬性中,具體可參考上述代碼示例中反編譯後stack=2
。
在一個方法剛開始執行的時候,這個方法的操做數棧是空的,在方法執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是說出棧/入棧操做。
動態連接是程序在運行期間將字節碼中的符號引用轉化爲直接引用的過程。Class 文件的常量池中有大量的符號引用,字節碼中的方法的調用指令調用常量池中存放的字面量符號,而這些字面量符號指向具體的方法。
一個方法開始執行後只有兩種方式能夠退出這個方法。
執行引擎遇到任意一個方法返回的字節碼指令,根據返回指令來決定是否將返回值和返回值的類型傳遞給上層的方法調用者。
通常來講,方法正常退出時,調用者的 PC 計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。
在方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理。異常完成出口的方式是不會給上層調用者產生任何返回值的。
異常退出時,返回地址是要經過異常處理表來肯定的,因此棧幀中通常不會保存這部分信息。
Java 堆是 Java 虛擬機所管理的內存中最大的一塊。堆爲全部線程共享的內存區域,在虛擬機啓動時建立。堆惟一的目的就是存放對象的實例,幾乎全部的對象都在堆內存區域存放。此區域也是發生 OOM 的重災區。
堆的內存結構能夠劃分爲新生代和老年代,默認1:2
,其中新生代又被細分爲 Eden
和兩個個Survivor
區域,兩個Servivor
分別以from
to
來進行區分,Eden:from:to 默認爲 8:1:1
。
JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,因此不管何時,老是有一塊 Survivor 區域是空閒着的。
方法區和堆同樣,是各個線程共享的內存區域,它用於儲存已被虛擬機加載的類信息(InstanceKlass)、常量、靜態變量、及時編譯期編譯後的代碼等數據(好比spring 使用IOC或者AOP建立bean時,或者使用cglib,反射的形式動態生成class信息等)。對於方法區的具體實現,會根據不一樣的 JVM 以及不一樣的版本而有所區別。在目前已經發布的JDK 1.7的HotSpot中,已經把本來放在永久代的字符串常量池移出。根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用。這部份內容將在類加載後進入方法區的運行時常量池中存放。運行時常量池具有動態性,在運行期間也能夠將新的常量放入池中,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。
補充說明:
方法區在邏輯上屬於堆的一部分,可是爲了與堆進行區分,一般又叫「非堆」(Non-Heap)。
永久代(Permanent Generation):永久帶是 Hotspot 虛擬機獨有的概念,由於 Hotspot 虛擬機把 GC 分代收集擴展至方法區,或者說用永久代來實現方法區。而對於其餘虛擬機(BEA JRockit IBM J9等)是沒有永久代的概念的。
一樣對於 Hotspot 虛擬機在jdk1.六、jdk1.七、jdk1.8 版本對方法區的實現時有所不一樣的:
- 永久帶的概念存在小於等於1.7 版本。對於字符串常量池來說 1.7 之前的版本存放於方法區,1.7版本字符串常量池和類的靜態變量存放於堆中,符號引用(Symbols)轉移到了native heap。
- 1.8 沒有了永久帶的概念,方法區又元空間(Metaspace)來實現,不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。
- 1.8 元空間在儲存內容方面和 1.7 沒有發生改變,依然靜態變量和字符串常量池在堆內存放,符號引用在 Native Heap 存放,元空間只存放儲類和類加載器的元數據信息。只是在內存限制、垃圾回收等機制上改變較大。元空間的出現就是爲了解決突出的類和類加載器元數據過多致使的OOM問題。
JDK 8 中永久代向元空間的轉換
- 字符串存在永久代中,容易出現性能問題和內存溢出。
- 類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。
- 永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
- Oracle 可能會將HotSpot 與 JRockit 合二爲一。
本文只是講述了一些 JVM 的內存結構,以及不一樣內存區域的基本概念,對於不一樣內存區域的參數調整,以及會出現怎麼樣的異常等內容會專門用一篇文章來說解。上面咱們已經講明白了JVM 內存區域的劃分以及概念,下面根據腦圖概括總結一下:
參考:
[1] 周志明.深刻理解Java虛擬機:JVM高級特性與最佳實踐.北京:機械工業出版社,2013.
[2] RednaxelaFX.Java多線程執行native方法時程序計數器爲空,那麼線程切換後如何找到以前執行到哪裏了?.
[3] RednaxelaFX.JVM符號引用轉換直接引用的過程?.
[4] secbro2 .JVM以內存結構詳解.
文章首發於陳建源的博客,歡迎訪問。
文章做者:陳建源
文章連接:https://www.techstack.tech/post/jvm-nei-cun-jie-gou/