java8內存結構圖
一、虛擬機內存與本地內存的區別
Java虛擬機在執行的時候會把管理的內存分配成不同的區域,這些區域被稱爲虛擬機內存,同時,對於虛擬機沒有直接管理的物理內存,也有一定的利用,這些被利用卻不在虛擬機內存數據區的內存,我們稱它爲本地內存,這兩種內存有一定的區別:
JVM內存
- 受虛擬機內存大小的參數控制,當大小超過參數設置的大小時就會報OOM
本地內存
- 本地內存不受虛擬機內存參數的限制,只受物理內存容量的限制
- 雖然不受參數的限制,但是如果內存的佔用超出物理內存的大小,同樣也會報OOM
二、Java運行時數據區域
java虛擬機在執行過程中會將所管理的內存劃分爲不同的區域,有的隨着線程產生和消失,有的隨着java進程產生和消失,根據《Java虛擬機規範》的規定,運行時數據區分爲以下一個區域:
程序計數器(Program Counter Register)
程序計數器就是當前線程所執行的字節碼的行號指示器,通過改變計數器的值,來選取下一行指令,通過他來實現跳轉、循環、恢復線程等功能。
-
在任何時刻,一個處理器內核只能運行一個線程,多線程是通過線程輪流切換,分配時間來完成的,這就需要有一個標誌來記住每個線程執行到了哪裏,這裏便需要到了程序計數器。
-
所以,程序計數器是線程私有的,每個線程都已自己的程序計數器。
虛擬機棧(JVM Stacks)
虛擬機棧是線程私有的,隨線程生滅。虛擬機棧描述的是線程中的方法的內存模型:
每個方法被執行的時候,都會在虛擬機棧中同步創建一個棧幀(stack frame)。
每個棧幀的包含如下的內:
-
局部變量表
-
局部變量表中存儲着方法裏的java基本數據類型(byte/boolean/char/int/long/double/float/short)以及對象的引用(注:這裏的基本數據類型指的是方法內的局部變量)
-
-
操作數棧
-
動態連接
-
方法返回地址
方法被執行時入棧,執行完後出棧。
虛擬機棧可能會拋出兩種異常:
- 如果線程請求的棧深度大於虛擬機所規定的棧深度,則會拋出StackOverFlowError即棧溢出。
- 如果虛擬機的棧容量可以動態擴展,那麼當虛擬機棧申請不到內存時會拋出OutOfMemoryError即OOM內存溢出。
三、本地方法棧(Native Method Stacks)
本地方法棧與虛擬機棧的作用是相似的,都會拋出OutOfMemoryError和StackOverFlowError,都是線程私有的,主要的區別在於:
-
虛擬機棧執行的是java方法
-
本地方法棧執行的是native方法(什麼是Native方法?)
四、Java堆(Java Heap)
java堆是JVM內存中最大的一塊,由所有線程共享,是由垃圾收集器管理的內存區域,主要存放對象實例,當然由於java虛擬機的發展,堆中也多了許多東西,現在主要有:
-
對象實例
-
類初始化生成的對象
-
基本數據類型的數組也是對象實例
-
-
字符串常量池
-
字符串常量池原本存放於方法區,jdk7開始放置於堆中。
-
字符串常量池存儲的是string對象的直接引用,而不是直接存放的對象,是一張string table
-
-
靜態變量
-
靜態變量是有static修飾的變量,jdk7時從方法區遷移至堆中
-
-
線程分配緩衝區(Thread Local Allocation Buffer)
-
線程私有,但是不影響java堆的共性
-
增加線程分配緩衝區是爲了提升對象分配時的效率
-
java堆既可以是固定大小的,也可以是可擴展的(通過參數-Xmx和-Xms設定),如果堆無法擴展或者無法分配內存時也會報OOM。
五、方法區(Method Area)
方法區絕對是網上所有關於java內存結構文章爭論的焦點,因爲方法區的實現在java8做了一次大革新,現在我們來討論一下:
方法區是所有線程共享的內存,在java8以前是放在JVM內存中的,由永久代實現,受JVM內存大小參數的限制,在java8中移除了永久代的內容,方法區由元空間(Meta Space)實現,並直接放到了本地內存中,不受JVM參數的限制(當然,如果物理內存被佔滿了,方法區也會報OOM),並且將原來放在方法區的字符串常量池和靜態變量都轉移到了Java堆中,方法區與其他區域不同的地方在於,方法區在編譯期間和類加載完成後的內容有少許不同,不過總的來說分爲這兩部分:
類元信息(Klass)
-
類元信息在類編譯期間放入方法區,裏面放置了類的基本信息,包括類的版本、字段、方法、接口以及常量池表(Constant Pool Table)
-
常量池表(Constant Pool Table)存儲了類在編譯期間生成的字面量、符號引用(什麼是字面量?什麼是符號引用?),這些信息在類加載完後會被解析到運行時常量池中
運行時常量池(Runtime Constant Pool)
-
運行時常量池主要存放在類加載後被解析的字面量與符號引用,但不止這些
-
運行時常量池具備動態性,可以添加數據,比較多的使用就是String類的intern()方法
六、直接內存
直接內存位於本地內存,不屬於JVM內存,但是也會在物理內存耗盡的時候報OOM,所以也講一下。
在jdk1.4中加入了NIO(New Input/Putput)類,引入了一種基於通道(channel)與緩衝區(buffer)的新IO方式,它可以使用native函數直接分配堆外內存,然後通過存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,這樣可以在一些場景下大大提高IO性能,避免了在java堆和native堆來回複製數據。
七、常見問題
什麼是Native方法?
由於java是一門高級語言,離硬件底層比較遠,有時候無法操作底層的資源,於是,java添加了native關鍵字,被native關鍵字修飾的方法可以用其他語言重寫,這樣,我們就可以寫一個本地方法,然後用C語言重寫,這樣來操作底層資源。當然,使用了native方法會導致系統的可移植性不高,這是需要注意的。
成員變量、局部變量、類變量分別存儲在內存的什麼地方?
類變量
-
類變量是用static修飾符修飾,定義在方法外的變量,隨着java進程產生和銷燬
-
在java8之前把靜態變量存放於方法區,在java8時存放在堆中
成員變量
-
成員變量是定義在類中,但是沒有static修飾符修飾的變量,隨着類的實例產生和銷燬,是類實例的一部分
-
由於是實例的一部分,在類初始化的時候,從運行時常量池取出直接引用或者值,與初始化的對象一起放入堆中
局部變量
-
局部變量是定義在類的方法中的變量
-
在所在方法被調用時放入虛擬機棧的棧幀中,方法執行結束後從虛擬機棧中彈出,所以存放在虛擬機棧中
由final修飾的常量存放在哪裏?
final關鍵字並不影響在內存中的位置,具體位置請參考上一問題。
類常量池、運行時常量池、字符串常量池有什麼關係?有什麼區別?
類常量池與運行時常量池都存儲在方法區,而字符串常量池在jdk7時就已經從方法區遷移到了java堆中。
在類編譯過程中,會把類元信息放到方法區,類元信息的其中一部分便是類常量池,主要存放字面量和符號引用,而字面量的一部分便是文本字符,在類加載時將字面量和符號引用解析爲直接引用存儲在運行時常量池;
對於文本字符來說,它們會在解析時查找字符串常量池,查出這個文本字符對應的字符串對象的直接引用,將直接引用存儲在運行時常量池;字符串常量池存儲的是字符串對象的引用,而不是字符串本身。
什麼是字面量?什麼是符號引用?
字面量
java代碼在編譯過程中是無法構建引用的,字面量就是在編譯時對於數據的一種表示:
int a=1;//這個1便是字面量
String b="iloveu";//iloveu便是字面量
符號引用
由於在編譯過程中並不知道每個類的地址,因爲可能這個類還沒有加載,所以如果你在一個類中引用了另一個類,那麼你完全無法知道他的內存地址,那怎麼辦,我們只能用他的類名作爲符號引用,在類加載完後用這個符號引用去獲取他的內存地址。
例子:我在com.demo.Solution類中引用了com.test.Quest,那麼我會把com.test.Quest作爲符號引用存到類常量池,等類加載完後,拿着這個引用去方法區找這個類的內存地址。