今天來了解一下jvm內存具體分佈及其意義。
1、虛擬機圖解:
2、解析各個區域
2.1 程序計數器(Program Counter Register):
每一個Java線程都有一個程序計數器來用於保存程序執行到當前方法的哪一個指令。
對於非Native方法,這個區域記錄的是正在執行的VM原語的地址;
如果正在執行的是Natvie方法,這個區域則爲空(undefined)。此內存區域是唯一一個在VM Spec中沒有規定任何OutOfMemoryError情況的區域。
2.2 Java虛擬機棧(Java Virtual Machine Stacks)(我們所說的棧在這裏)
與程序計數器一樣,VM棧的生命週期也是與線程相同。VM棧描述的是Java方法調用的內存模型:每個方法被執行的時候,都會同時創建一個幀(Frame)用於存儲本地變量表、操作棧、動態鏈接、方法出入口等信息。每一個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。
在後文中,我們將着重討論VM棧中
2.2.1 本地變量表部分
經常有人把Java內存簡單的區分爲堆內存(Heap)和棧內存(Stack)
,實際中的區域遠比這種觀點復
雜,這樣劃分只是說明與變量定義密切相關的內存區域是這兩塊。其中所指的“堆”後面會專門描述,而所指的“棧”就是VM棧中各個幀的本地變量表部分
。本地變量表存放了編譯期可知的各種標量類型
(boolean、byte、char、short、int、float、long、double)、對象引用(不是對象本身,僅僅是一個
引用指針)、方法返回地址等。其中long和double會佔用2個本地變量空間(32bit),其餘佔用1個。
本地變量表在進入方法時進行分配,當進入一個方法時,這個方法需要在幀中分配多大的本地變量是一件完全確定的事情,在方法運行期間不改變本地變量表的大小。
- 本地變量表的最小存儲單元爲Slot(槽),其中64位長度的long和double類型的數據會佔用2個Slot,其餘的數據類型只佔用1個。所以我們可以將局部變量表分爲一個個的存儲單元,每個存儲單元有自己的下標位置,在對數據進行訪問時可以直接通過下標來訪問
在VM Spec中對這個區域規定了2種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果VM棧可以動態擴展(VM Spec中允許固定長度的VM棧),當擴展時無法申請到足夠內存則拋出OutOfMemoryError異常。
2.2.2 操作數棧
操作數棧主要用於保存計算過程中的中間結果,同時作爲計算過程中變量臨時的存儲空間。
操作數棧也是一個先進後出的數據結構,只支持入棧和出棧兩種操作。
把局部變量區的東西拿過來入棧,出棧等等,如
a =2;
b = 3;
c = a + b;
return c;
會把局部變量表的a 和 b拿過來入棧,進行運算,得到的c保存在棧中。
操作數棧對於數據的存儲跟本地變量表是一樣的,但是跟本地變量表不同的是,操作數棧對於數據的訪問不是通過下標而是通過標準的棧操作來進行的(壓入與彈出),之後在分析字節碼指令時我們會很明顯的感覺到這一點。另外還有,對於數據的計算是由CPU完成的,所以CPU在執行指令時每次會從操作數棧中彈出所需的操作數經過計算後再壓入到操作數棧頂。
2.2.3 演示i++與++i的區別(瞭解棧幀裏的過程)
一個方法有一個棧幀,n個方法n個棧幀。
例子
package 面試題;
public class Practice_Jvm {
public static void main(String []args){
int i = 2;
int b = i++;//2
int c = ++i;//4?
System.out.println(i+" "+b+" "+c);
}
}
我們來編譯運行看一下結果:
D:\編程\java>java Practice_Jvm
4 2 4
使用javap命令查看字節碼文件:
D:\編程\java>javap -c Practice_Jvm.class
Compiled from "Practice_Jvm.java"
public class Practice_Jvm {//一個方法一個棧幀
public Practice_Jvm();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2 //常量2入棧
1: istore_1 //將棧頂元素(值爲2)彈出到本地變量表中的下標爲1的Slot(槽),完成i的賦值 i = 2
2: iload_1 //將1槽的數壓入棧(值爲2)
3: iinc 1, 1 //將1槽裏的i值加1;此時i=3;
6: istore_2 //將棧頂的元素值(值爲2)彈出到2槽,即讓b = 2;
7: iinc 1, 1 /將1槽的值加1,此時i=4
10: iload_1 //1槽的值入棧,
11: istore_3 //將棧頂元素值(i=4)彈出到3槽,即c=4
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_1
16: iload_2
17: iload_3
18: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(III)Ljava/lang/String;
23: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: return
}
現在我們修改代碼爲:
public class Practice_Jvm {
public static void main(String []args){
int i = 2;
int b = i++;//2
int c = ++i;//4
System.out.println(i+" "+b+" "+c);
for(int k=0;k<10;k++){
b = b++;//注意這條語句
}
System.out.println(b);//b則等於2?還是12?
}
}
運行結果:
老規矩,先看字節碼:
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iload_1
3: iinc 1, 1
6: istore_2
7: iinc 1, 1
10: iload_1
11: istore_3
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: iload_1
16: iload_2
17: iload_3
18: invokedynamic #3, 0 // InvokeDynamic #0:makeConcatWithConstants:(III)Ljava/lang/String;
23: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: iconst_0 //0入棧
27: istore 4 //將棧頂元素0彈出到4槽,完成k的賦值
29: iload 4 //4槽的k值入棧
31: bipush 10 //條件判斷
33: if_icmpge 47 //不滿足就跳至47行,這裏的計算與判斷由cpu進行
36: iload_2 //2槽的b的值 2 入棧
37: iinc 2, 1 //2槽的b值加一,b = 2+1 = 3;
40: istore_2 //將棧頂元素彈出到2槽,b被重新賦值,b = 2;
41: iinc 4, 1 //4槽的k加1,k=1
44: goto 29 //跳轉到29行,k入棧
47: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
50: iload_2
51: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
54: return
通過觀察36,37,40行的代碼,我們可以知道,int b = b++是相當於不改變b的值。
2.2.4 總結
我們平常說的i++是先拿去用,然後再自增,而++i是先自增再拿去用。這個到底怎麼理解呢?如果站在JVM的層次來講的話,應該這樣說:
i++是先被操作數棧拿去用了(先執行的load指令),然後再在局部變量表中完成了自增,但是操作數棧中還是自增前的值
而++1是先在局部變量表中完成了自增(先執行innc指令),然後再被load進了操作數棧,所以操作數棧中保存的是自增後的值
圖中局部變量表的下標都是從1開始,這是因爲我直接用main函數測試的。
局部變量表中下標爲0的元素是main函數中的形參,也就是String[]args。
另外也通過這些過程我們也可以發現,局部變量表就是通過下標訪問的,而操作數棧就是通過正常的棧操作(壓入/彈出)來完成數據訪問的
2.3 本地方法棧(Native Method Stacks)
本地方法棧與VM棧所發揮作用是類似的,只不過VM棧爲虛擬機運行VM原語服務,而本地方法棧是爲虛擬機使用到的Native方法服務。它的實現的語言、方式與結構並沒有強制規定,甚至有的虛擬機(譬如SunHotspot虛擬機)直接就把本地方法棧和VM棧合二爲一。和VM棧一樣,這個區域也會拋出StackOverflowError和OutOfMemoryError異常。
2.4 Java堆(Java Heap)
對於絕大多數應用來說,Java堆是虛擬機管理最大的一塊內存。Java堆是被所有線程共享的,在虛擬機啓動時創建。Java堆的唯一目的就是存放對象實例,
**絕大部分的對象實例都在這裏分配。(並非全部)**這一點在VM Spec中的描述是:所有的實例以及數組都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和標量替換優化技術
出現後,VM Spec的描述就顯得並不那麼準確了。
Java堆內還有更細緻的劃分:新生代、老年代,再細緻一點的:eden、from survivor、to survivor,甚至更細粒度的本地線程分配緩衝(TLAB)等,無論對Java堆如何劃分,目的都是爲了更好的回收內存,或者更快的分配內存。
根據VM Spec的要求,Java堆可以處於物理上不連續的內存空間,它邏輯上是連續的即可
,就像我們的磁
盤空間一樣。實現時可以選擇實現成固定大小的,也可以是可擴展的,不過當前所有商業的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中無法分配內存,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
2.5 方法區(Method Area)
叫“方法區”可能認識它的人還不太多,如果叫永久代(Permanent Generation)它的粉絲也許就多了。它還有個別名叫做Non-Heap(非堆),但是VM Spec上則描述方法區爲堆的一個邏輯部分(原文:the method area is logically part of the heap),這個名字的問題還真容易令人產生誤解,我們在這裏就不糾結了。
方法區中存放了每個Class的結構信息,包括常量池、字段描述、方法描述等等。
VM Space描述中對這個區域的限制非常寬鬆,除了和Java堆一樣不需要連續的內存,也可以選擇固定大小或者可擴展外,甚至可以選擇不實現垃圾收集。相對來說,垃圾收集行爲在這個區域是相對比較少發生的,但並不是某些描述那樣永久代不會發生GC(至少對當前主流的商業JVM實現來說是如此),這裏的GC主要是對常量池的回收和對類的卸載,雖然回收的“成績”一般也比較差強人意,尤其是類卸載,條件相當苛刻。
2.5.1 運行時常量池(Runtime Constant Pool)
Class文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類加載後進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。
運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法在申請到內存時會拋出OutOfMemoryError異常。
2.6 本機直接內存(Direct Memory)
直接內存並不是虛擬機運行時數據區的一部分
,它根本就是本機內存而不是VM直接管理的區域。
但是這部
分內存也會導致OutOfMemoryError異常出現,因此我們放到這裏一起描述。
在JDK1.4中新加入了NIO類,引入一種基於渠道與緩衝區的I/O方式,它可以通過本機Native函數庫直接分配本機內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和本機堆中來回複製數據。
顯然本機直接內存的分配不會受到Java堆大小的限制,但是即然是內存那肯定還是要受到本機物理內存(包括SWAP區或者Windows虛擬內存)的限制的,一般服務器管理員配置JVM參數時,會根據實際內存設置Xmx等參數信息,但經常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),而導致動態擴展時出現OutOfMemoryError異常。
#參考博客#
細說JVM系列:JVM內存空間分區
JVM內存模型及分區的理解
面試官:你說你懂i++跟++i的區別,那你會做下面這道題嗎?
參考書本
百度網盤(驗證碼:d6sy) :深入java虛擬機.pdf