運行數據區域:
JVM在運行過程中會把它所管理的內存劃分成幾個不同的運行數據區域。其中,有線程共享的堆和方法區,還有線程私有的虛擬機棧、本地方法棧、和程序計數器。
1.程序計數器:
其可以看作是當前線程所執行字節碼的行號指示器,該區域是線程私有的,爲了準確記錄各個線程正在執行字節碼指令的地址,多線程下各個線程就互不干擾。
如果線程正執行一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址。
如果執行的是Native方法,這個計數器值爲空(undefined)
此內存區域是唯一沒有OOM的區域
2.虛擬機棧:
虛擬機棧描述的是Java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
在IDea中debug可以看到,每次調用方法都會創建一個棧幀,每個棧幀是先進後出。
棧幀在虛擬機的入棧到出棧,就是Java方法從調用到執行完成的過程。
局部變量表: 存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不等同於對象本身,根據不同的虛擬機實現,它可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址)。
局部變量表所需要的內存空間在編譯期完成分配,當進入一個方法時,這個方法在棧中需要分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表大小。
大小: 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。
虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。
3.本地方法棧:
如同虛擬機棧爲虛擬機執行Java方法服務一樣,本地方法棧爲虛擬機使用到的Native方法服務
4.Java堆:
該內存區域的唯一目的就是 存放對象實例,同時它也是GC(垃圾收集管理器)所管理的主要區域。Java堆中還可以細分爲新生代和老年代等等,不做深究。
5.方法區:
用於存儲已被虛擬機加載的類信息(類的版本、字段、方法等)、常量、靜態變量,如static修飾的變量加載類的時候就被加載到方法區中。
在類加載的加載階段,將.Class文件轉換爲方法區的運行時數據結構。
運行時常量池:
運行時常量池是方法區的一部分,.class文件除了有類的字段、接口、方法等描述信息之外,還有常量池,其用於存
放編譯期間生成的各種字面量和符號引用,這部分內容在類加載(加載階段)後進入方法區的運行時常量池。
int a = 1; //1 就是字面量
String a = “abc”; //abc 就是字符串字面量。
運行時常量池具有動態性,運行期間也可以將新的常量放入池中,比如String.intern()方法。舉個栗子:
public class Test{
public static void main(String[] args){
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
String s3 = new String("abc");
System.out.println(s1 == s3);
System.out.println(s1 == s3.intern());
}
}
abc是字符串字面量,存放於運行時常量池中;s1存放於棧(虛擬機棧)中,並指向abc;代碼又上往下執行,s2存
放於棧中,並指向運行時常量池中同一個abc。== 比較的是引用地址,所以s1==s2爲true;
new新建會在Java堆中開闢看塊新內存存儲abc,s3作爲對象引用存儲在棧中,顯然,s1 == s3爲false。
s3.intern()會將堆中abc放入運行時常量池,因爲abc在池中本來就有了,所以其引用地址和s1是一樣的,所以結果
爲true。
Java對象探祕:
1.對象的創建:
.java文件編譯後生成的各種字面量和符號引用,這部分內容在類加載(加載階段)後進入方法區的運行時常量池。
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用表的類是否已被加載,解析和初始化過。如果沒有,那必須先執行相應的類加載過程。在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。
類加載的生命週期:加載、連接(驗證、準備、解析)、初始化、使用和卸載
遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用(如果沒有,則執行類加載的加載階段)
檢查這個符號引用表的類是否已被加載,解析和初始化過(如果沒有,則執行相應的解析、初始化階段)。
在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。
內存分配方式:
1). 指針碰撞(Java堆內存規整):
所有已使用的內存放在一邊,空閒的在另一邊,中間放着一個指針作爲分界點的指示器,那所分配的內存僅僅就是
把那個指針向空閒的那邊挪動一段與對象大小相等的距離
2). 指針碰撞(Java堆內存不規整):
虛擬機維護一個列表,記錄哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間會分給對象實例,
並更新列表記錄。
線程安全解決:
1). 線程安全問題:
虛擬機正給對象A分配內存,指針還沒來得及修改,對象B又同時使用原來的指針來分配內存的情況。
2). 解決:
調用對象初始化方法:
開始執行< init >方法進行對象的初始化,按照程序員的意願初始化對象,至此一個真正可用的對象纔算產生。
2.對象的內存佈局:
對象在內存中存儲的佈局分3塊區域:對象頭、實例數據(對象真正存儲的有效信息)、對齊填充(佔位用的)。
對象頭(Header): 包括用於存儲對象自身的運行時數據(Mark World)和類型指針。
1). 自身運行時數據(Mark World): 包括哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖等等。
2). 類型指針: 對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
3.對象的訪問定位:
1). 使用句柄: Java堆中劃分一塊內存作爲句柄池,reference中存儲的是對象的句柄地址,而句柄中包含對象實例數據和類型數據各自的具體地址。
2). 直接指針: reference中存儲的直接就是對象的地址,HotSpot虛擬機主要使用的。
3). 二者優點:
舉個栗子:
Student類:
public class Student{
public String name;
public Student(String name){
this.name = name;
}
public void sayWord(String Word){
//Word="helloWord";
}
}
程序入口類:
public class APP{
public static void main(String[] args){
Student stu = new Student("abc");
String s1 = "hello";
stu.sayWord(s1);
System.out.println(s1);
}
}
過程:
1). 編譯好 App.java 後得到 App.class 後,執行 App.class,系統會啓動一個 JVM 進程,從 classpath 路徑中找到一個名爲 App.class 的二進制文件,將 App 的類信息加載到運行時數據區的方法區內(類加載)。
2). JVM 找到 App 的主程序入口,執行main方法。
3). 自上而下執行,遇到new指令,發現方法區的運行時常量池沒有Student類的符號引用,則先加載Student類(可見類加載是懶加載)
4). 加載完 Student 類後,JVM 再在堆中爲一個新的 Student 實例分配內存,然後調用< init >()構造函數初始化 Student 實例。
5). (直接指針)此時棧中存儲對象引用stu,指向堆中Student的地址,這個 Student 內存區域(實例)持有實例數據(如name = abc)和指向方法區中的 Student 類的類型數據的指針。
6). 執行String s1 = “hello”; 在main()棧幀的局部變量表存儲了s1(字符串爲引用類型),指向方法區的字符串字面量"hello"
6).執行stu.sayWord(s1);時,JVM 根據 stu 的引用找到 student 對象,然後根據 student 對象持有的引用定位到方法區中 student 類的類型信息的方法表,獲得 sayWord(s1) 的字節碼地址。
7). 執行sayWord(s1)。
8). 虛擬機棧創建一個棧幀sayWord,該棧幀的局部變量表存儲了Word,指向方法區的字符串字面量"hello",方法調用即入棧,調用完畢即出棧。
如果在sayWord方法中修改了Word=“helloWorld"的值,實際是在常量池中新開闢了"helloWorld"的內存,Word重新指向"helloWorld”,所以在main()中打印s1還是 “hello”
參考:
《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版)》
大部分內容均來自這本書,下圖爲思維導圖。
互相交流,互相學習,有誤指正。