背景:一談到JVM一直是很多人覺得頭疼的知識點,那麼針對JVM這個痛點,我總結了一些,網上很多談到由淺入深JVM,其實醜話說在前,一篇文章或者幾篇文章是不夠深入JVM的,但至少知其然。
PS:至於知其所以然,依舊還是推薦《深入理解JVM》這本書,雖說它很多還是基於JDK1.7去演示的,但萬變不離其宗。且目前已有更新第三版,完全不用擔心過時。周老師還是很強滴~~
一、JVM內存結構組成
首先我們來看一張圖。由圖我們可得知,JVM組成主要包含 堆、棧、元區間(方法區)、本地方法棧、PC寄存器等。
且JVM內存中包含 棧、本地方法棧、PC寄存器。且在1.8之前是包含方法區的。1.8之後揪出來放在了內存。
1.1、堆
堆:只要new一個對象,就會存放在堆裏。比如定義一個數組,堆數據所有線程都會共享。在Java虛擬機啓動就建立堆,最主要的內存工作區域,幾乎所有的對象實例都存放到Java堆中。我們可以認爲堆中的變量是持久存在的,而棧的變量是臨時態的。至於老年代新生代垃圾回收後期記載。
比如創建一個數組array:
public int[] array = new int[] {1, 2, 3};
那麼該對象array就會存放在堆內存中,被所有線程共享。由此也會引發一個很頭疼的問題,當類A與類B同時操作該數組時,會出現衝突,造成數據錯亂,那麼就產生線程安全問題。讓人頭疼。當然解決的方法也有很多種,JDK中syn鎖,lock鎖等都可以處理,這裏跳過。
1.2、棧
棧:棧她由一個個棧幀組成,那麼棧幀是什麼?其實就是一個一個的方法體,比如方法say,就是一個棧幀。
public static void say(String text){
String remark = "Hello world";
System.out.println(remark + text);
}
棧幀(方法體):那麼通過方法體來形象的理解棧幀。每個棧幀都包含 局部變量表、操作數棧、動態鏈接、返回鏈接。
- 局部變量表:用於方法參數和方法內部定義的局部變量。通過索引訪問。Say方法中的text參數與變量remark屬於局部變量
- 操作數棧:又稱操作棧,通過標準彈棧壓棧進行訪問。比如,如果某個指令把一個值壓入到操作數棧中,稍後另一個指令就可以彈出這個值來使用。看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的。
begin
iload_0 // push the int in local variable 0 onto the stack
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
在這個字節碼序列裏,前兩個指令iload_0和iload_1將存儲在局部變量中索引爲0(100)和1(98)的整數壓入操作數棧中,其後iadd指令從操作數棧中彈出那兩個整數相加(100+98),再將結果壓入操作數棧。第四條指令istore_2則從操作數棧中彈出結果(198),並把它存儲到局部變量區索引爲2的位置。下圖詳細表述了這個過程中局部變量和操作數棧的狀態變化,圖中沒有使用的局部變量區和操作數棧區域以空白表示。
看完以上一堆代碼我們再來簡單複述下,它對應的Java代碼實際上就是
public static int add(){
int i0 = 100;
int i1 = 98;
// int i2 = i0 + i1; return i2;
//198
return i0 + i1;
}
是不是很簡單?
- 動態鏈接:動態鏈接的概念,就相當於你在say方法中,調用了add方法,來段代碼
public static void print(){
System.out.println(add());
}
- 返回鏈接:即指方法運行後,返回某處。以print方法爲栗子,add方法返回鏈接就指向print,而print是無返回void方法,那麼程序會直接運行完畢。若是在main方法中運行print就會返回到main方法,並繼續走完程序。
1.3、本地方法棧
本地方法棧,最大不同爲本地方法棧用於本地方法調用。Java虛擬機允許Java直接調用本地方法(通過使用C語言寫)。
上圖說到Native修飾的方法就是本地方法,那麼有哪些呢?舉個最通俗的例子。String類源碼中的intern方法。
public native String intern();
那麼該方法就是個本地方法,至於使用與作用,在下文方法區詳細講解。
1.4、元區間(方法區)
方法區主要存放類的信息、常量信息、常量池信息、包括字符串字面量和數字常量等。
那麼舉個栗子來了解一下方法區
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
String d = "a";
System.out.println(a == b);//true
System.out.println(a == c);//false
System.out.println(a == c.intern());//true
System.out.println(a == d);//false
}
下面畫圖演示下ABC三者的關係圖
首先根據a、b、c屬於方法內局部變量,那麼就是存放在棧中。其次是方法區字符串常量池中的"abc",是由方法內部創建而來。
至於堆中也會有個"abc",那是由於 c是通過new String()創建的。
細心的朋友會發現,代碼中,a==c爲false,a==c.intern()爲什麼會爲true呢?
原因:調用了intern方法的字符串會將堆的值放入到常量池中,原理,被native關鍵字修飾。其實就是將JVM內存中的數據放入了本地內存中。相當於Hashset賦值,堆裏面的數據就不會有了。
通過c.intern後的引用圖狀態。
千萬注意!!! JDK1.6及之前是false,JDK1.7及以後爲true。面試如果有指定JDK的坑的intern相關筆試題,就不要弄錯了!
1.5、PC寄存器
PC(Program Couneter)寄存器也是每個線程私有的空間, Java虛擬機會爲每個線程創建PC寄存器,在任意時刻,一個Java線程總是在執行一個方法,這個方法稱爲當前方法,如果當前方法不是本地方法,PC寄存器總會執行當前正在被執行的指令,如果是本地方法,則PC寄存器值爲Underfined。
寄存器存放當前執行環境指針、程序技術器、操作棧指針、計算的變量指針等信息。
通俗點說,PC寄存器即爲程序執行位置。
二、彙編
在上文中的棧幀中提到了彙編的操作,相信大家應該也是迫不及待的想知道原理,也想自己動手操作呢!那麼教程
比如現在要彙編Test類
public class Test {
/*
彙編代碼執行
0: bipush 100 將一個8位帶符號整數壓入棧(這裏是壓入100)
2: istore_1 將int類型值存入局部變量(其他類型有其他的規則)
3: bipush 99 同100壓入
5: istore_2 將int類型值存入局部變量(其他類型有其他的規則)
6: iload_1 從局部變量中裝載int類型值 (將100裝載到操作數棧)
7: iload_2 從局部變量中裝載int類型值 (裝99載到操作數棧)
8: iadd 執行int類型的加法 (99+100)
9: istore_3 將int類型值存入局部變量(其他類型有其他的規則) 定義給第三個變量 c,存入局部變量
10: return 方法返回
*/
public void add(){
int a = 100;
int b = 99;
int c = a + b;
}
public static void main(String[] args) {
Test t = new Test();
t.add();
}
}
1.首先我們切到對應目錄輸入命令,注意是當前Test類在的目錄
java -c Test.java
2.我們會發現出現不是處理文件,那麼我們打開設置
4是名字,5是備註都可以更改
4:JavapUser
5:執行Javap命令,我要彙編我不管必須要行
6:$JDKPath$\bin\javap.exe
7:-v $FileClass$
8:$OutputPath$
3.編輯完成後,我們運行命令也不會成功的,至少我不會,那麼如何做呢?
右鍵呼出菜單,選擇External Tools點擊顯示的命令即可
三、總結
JVM內存中主要包含棧(由多個棧幀組成,一個棧幀等於一個方法)、本地方法棧(Native修飾的方法,如String源碼中的intern)、PC寄存器(程序執行位置),JDK1.7之前包含方法區(存放class對象、靜態屬性、常量池等等)。
1.8之後方法區移動到內存中,更名元區間,與堆(存放new對象)共處一室。