上面已經聊過JVM是什么東東,也談過了JVM內存的垃圾回收機制。這一篇博客我們來聊聊JVM運行時數據區域。
JVM運行時數據區域由5塊部分組成,分別是堆,方法區,棧,本地方法棧,以及程序計數器組成。
可以根據內存是否線程共享划分成線程獨享內存區域/線程共享內存區域。
我們從簡單的部分開始吧
1.程序計數器
特點:線程內存獨享,占用內存小,生命周期與線程相同(隨線程誕生而誕生,隨線程消亡而消亡)
功能:當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復(cpu在不斷輪詢執行任務)等基礎功能都需要依賴這個計數器來完成
異常:該區域沒有定義異常
2.棧(方法執行的動態模型)
特點:先進后出,線程內存獨享,生命周期與線程相同
單位:棧幀
功能:已先進后出執行方法體的方法,執行完成的棧幀出棧
舉例:
public static void main(String[] args) { a();//調用a方法 b();//調用b方法 }
執行順序
1.main函數的棧幀入棧
2.a方法的棧幀入棧
3.a方法執行完成后,a棧幀出棧
4.b方法的棧幀入棧
5.b方法執行完成后,b棧幀出棧
6.main方法執行完成后,main棧幀出棧,程序結束
異常:StackOverFlow(棧溢出),OutOfMemory(可以擴展棧內存的情況下,內存溢出)
接下來我們來談談棧的基本單位棧幀吧
棧幀(每一個方法對應一個棧幀)
只有虛擬機棧頂的棧幀才是有效的,稱為當前棧幀 (Current Stack Frame),這個棧幀所關聯的方法稱為當前方法(Current Method)
組成:局部變量表,操作數棧,動態鏈接,方法出口信息4部分組成
1.局部變量表:由基本數據類型和對象引用組成的
作用:用來存儲方法中的局部變量
基本單位:slot
局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。
如果存儲的是基本數據類型那么直接存儲值
如果存儲的是對象引用那么存儲對象的引用地址( reference)(堆中)
補充:比較reference的兩種實現方式
直接引用 vs 使用句柄池
直接引用
reference直接指向對象,對象中指向對象類型數據
優點:速度快,節約指針開銷。HotSpot采用的主要方式
使用句柄池:
java堆中會維護一個句柄池,句柄池分別指向對象實例(堆)的和對象類型數據(方法區)
優點:對象移動后只需改變句柄池的指向地址,而不需要改變引用的指向地址。穩定
2.操作數棧
操作數棧的深度在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。
功能:實現程序功能
3.動態連接
補充下直接引用與符號引用
直接引用:當類已經加載到虛擬機時,通過地址直接調用該類
符號引用(常量池中):在編譯的時候還不知道類是否被加載,先用符號代替該類,等實際運行時再用直接引用替換間接引用。
靜態解析:符號引用一部分會在類加載階段或第一次使用的時候轉化為直接引用
動態連接: 將在每一次的運行期期間轉化為直接引用
4.方法出口信息
當一個方法執行完畢之后,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。
但是出現異常會不會返回地址
補充:
局部變量,在方法內部聲明,當該方法運行完時,內存即被釋放。
成員變量,只要該對象還在,哪怕某一個方法運行完了,還是存在。
從系統的角度來說,聲明局部變量有利於內存空間的更高效利用(方法運行完即回收)。
成員變量可用於各個方法間進行數據共享。
補充:棧幀的部分操作數棧與上面棧幀的部分局部變量表重疊在一起,這樣在進行方法調用返回時就可以共用一部分數據,而無須進行額外的參數復制傳遞了
{ int a=0; a+=10; method(a); }void method(int num) { System.out.print(a); }
通過這個例子我們看到如果不重疊的話,每次都要重新計算a+=10的值才能執行下面的方法。
3.本地方法棧
大體上都類似於虛擬機棧
不同點:棧執行的java方法服務
本地方法棧執行的是Native方法(不一定是用java開發的)服務
4.堆
特點:存儲對象,線程間內存共享,占用大量內存,垃圾回收關注的重點區域
關於的堆的分類可以參考上一篇的java垃圾回收機制
異常:OutOfMemoryError
每次都向堆中存放對象,方法結束后,銷毀棧幀的局部變量表時同時銷毀引用,該對象就成了可回收的垃圾。咋看起來沒什么不對呀,可是仔細思考下還是存在兩個問題
1.不斷的來回增加刪除對象,對於GC的工作量太大。
2.java使指針碰撞(堆中存入新對象的時候,指針根據對象大小移動到相應位置)來為對象分配內存。如果在多線程的環境下,就會出現兩個對象同時移動當前前指針的情況,造成線程不安全的情況。
這里就要引入TLAB的概念了
TLAB的全稱是Thread Local Allocation Buffer,這是一個線程專用的內存分配區域。每個線程都會從Eden分配一塊空間,當線程銷毀時,我們自然可以回收掉TLAB的內存。
使用TLAB指令 -XX:UseTLAB
優點:線程安全,減少垃圾回收的壓力。
缺點:TLAB空間大小是固定的,面對大對象的時候不夠靈活
5.方法區
特點:存儲類,線程間內存共享
存放已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
異常:OutOfMemoryError
注意:JDK 6 時,String等字符串常量的信息是置於方法區中的,但是到了JDK 7 時,已經移動到了Java堆。所以,方法區也好,Java堆也罷,到底詳細的保存了什么,其實沒有具體定論,要結合不同的JVM版本來分析。
提到方法區不得不說的就是常量池
補充:方法區不是永久代,只是Hotspot的實現方式而已。
常量池
什么是常量?
常量是指被final修飾的變量,值一旦確定就無法改變。
Class文件中的常量池
常量池主要用於存放兩大類常量:字面量和符號引用量(在上面已經介紹過了),字面量相當於Java語言層面常量的概念(如文本字符串,聲明為final的常量值等),符號引用則屬於編譯原理方面的概念
運行時常量池
class文件中的常量池中的內容會在類加載后進入方法區的運行時常量池。
相對於常量池,運行時常量池的重要特征是具有動態性,java並不要求常量只有在編譯器才會產生,運行期間也可以將新的常量存放入池中,這種特性用的最多的String類中的intern()方法。
那么變量存放在哪里?
對於字符串:其對象的引用都是存儲在棧中的,如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常量池中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。
int i1 = 9; int i2 = 9; int i3 = 9; public static final int INT1 = 9; public static final int INT2 = 9; public static final int INT3 = 9;
對於基礎類型的變量和常量:變量和引用存儲在棧中,常量存儲在常量池中。
int i1 = 9; int i2 = 9; int i3 = 9; public static final int INT1 = 9; public static final int INT2 = 9; public static final int INT3 = 9;
補充:最后一個疑問,jvm怎么調用方法
類加載機制鏈接部分,在類加載的准備階段,會為靜態字段分配內存,還會構造與該類相關聯的方法表
這個數據機構就是java虛擬機實現動態綁定的關鍵所在。
方法表本質上是一個數據,每個數據元素指向一個當前類以及其祖先類中非私有的實例方法
方法表的兩個特質
1,子類方法表中包含父類方法表中的所有方法
2,子類方法在方法表中的索引值,與它所重寫的父類方法的索引值相同
索引值
方法調用指令中的符號引用會在執行之前解析成實際引用。對於靜態綁定的方法調用而言,實際引用將指向具體的目標方法,對於動態綁定的方法調用而言,實際引用則是方法表的索引值
在執行過程中,java虛擬機將獲得調用者的時間類型,並在該時間類型的虛方法表中,根據索引值獲得目標方法。這個過程便是動態綁定