JVM 之 運行時數據區
我們都知道Java是一門多線程的編程語言,而這一切離不開底層JVM的多線程支持。如果想更好的理解JVM的多線程模型,以及方法的調用和執行原理。那么就必須先很好的了解JVM的運行時內存布局。
目錄
第一篇 JVM 之 Class文件結構
JVM定義了一系列程序運行期間使用的運行時數據區(run-time data area)。這些數據區域中的一些隨著JVM的啟動而創建直到JVM的停止而銷毀,而另一些則隨著某個線程的創建而創建,隨著線程的銷毀而銷毀。
為了能更直觀的了解JVM的運行時數據區,我們先上張圖來瞅瞅整個JVM內存的邏輯布局:
以上僅是一個JVM運行時內存布局的概念模型,我們可以看出JVM主要定義5大類運行時數據區:
1)虛擬機棧,2)方法區,3)本地方法棧,4)堆,5)程序計數器。
當然除了這幾個數據區還有1)運行時常量池,2)幀,3)本地變量表,4)操作數棧等數據區,下面我們都會一一分析。
對于上圖個人覺得除了看到這幾塊區域,也沒什么深入的細節上的信息了,而且很多情況下還會誤導初學者,比如很多人認為虛擬機棧就那么一塊區域,其實不然,而且虛擬機棧可以是不連續的。
因此作為一名程序員,個人一直認為代碼是最好的注釋和文檔,一行代碼勝過千言萬語。因此為了更好的理解JVM的內存模型,我們下面用JAVA代碼的形式來深入分析下。
零,JVM
像上一篇文章一樣,我們還是從整體著手,然后到具體的細節逐個分析。下面就是一個可能的JVM內存的Java實現的類結構圖:
下面我們逐個列出每個數據區域的類實現來(注:該實現只是一個用來幫助理解的模型,會忽略很多細節,并且可能有不正確的地方,歡迎討論)
//JVM.java public class JVM { private Heap heap; private MethodArea methodArea; private Map<String, NativeMethodStack> nativeMethodStacks; private Map<String, VMStack> vmStacks;//假設線程名為鍵 private Map<String, PCRegister> pcRegisters;//假設線程名為鍵 //....getter, setter }
上述代碼很簡單,清晰明了,不多說了。接下來我們就逐個深入分析。
一,Heap(堆)
堆是虛擬機中線程共享的一塊數據區域,也就說所有的線程都可以訪問這塊區域的數據。同時堆是虛擬機中用來對象和數組分配內存的地方。堆的生命周期跟虛擬機一樣,在虛擬機啟動時創建,在虛擬機關閉時銷毀。另外虛擬機中的對象無需顯示的進行內存回收,JVM垃圾回收器會自動回收那些‘不用的’對象和數組。為更好的實現來及回收機制,通常JVM的實現會將堆內存劃分為新生代(New Generation)和老年代(Tenured Generation),而新生代中又分為Eden Area和Survivor Area。下面我們看下堆內存的結構:
//堆 public class Heap { private long xms;//min heap size private long xmx;//max heap size private NewGeneration newGenration;//新生代 private TenuredGeneration tenuredGeneration;//老年代 } //新生代 public class NewGeneration { private int survivorRatio;// = (eden size / survivor size) private long xmn;//new generation private EdenArea eden; private SurvivorArea fromSurvivor; private SurvivorArea toSurvivor; } //老年代 public class TenuredGeneration { private byte[] memory; } //Eden public class EdenArea { private byte[] memory; } //Survivor public class SurvivorArea { private byte[] memory; }
相信看到代碼后你會感覺更加直觀了。
二,VM Stack(虛擬機棧)
JVM虛擬機棧是線程私有數據區域,也就是每個線程都有一個自己的虛擬機棧內存,該內存隨著線程的創建而創建,隨著線程的銷毀而銷毀。虛擬機棧用來存儲棧幀(frame),而棧幀會在下文詳解。虛擬機棧類似于傳統語言(如C)中的棧。它主要用來完成方法的調用和返回。由于虛擬機棧除了push和pop棧幀沒有其他操作,所以虛擬機棧的內存可以是不連續的。下面是虛擬機棧的Java代碼結構
import java.util.Stack; public class VMStack { private Thread owner; private long stackDeep;//最大棧容量 private Stack<Frame> frames; }
廢話不多說,繼續往下說,既然提到棧幀,我們就看看什么是棧幀。
三,Frame(棧幀)
棧幀用來存儲方法執行期間的數據和部分結果,同時還會執行動態鏈接,返回方法返回值,以及分派異常等動作。
每當有方法被調用時,就會創建一個新的棧幀,并壓入執行該方法的線程的虛擬機棧中。當方法執行結束后,該棧幀就會被彈出并銷毀,無論該方法是正常結束還是異常退出。每個棧幀內部都有一個本地變量表和操作數棧,以及一個指向運行時常量池中表示當前方法及所屬類的常量的引用。(本地變量表,操作數棧,運行時常量池將在下文分析)
本地變量表和操作數棧的大小在編譯期就會被確定,并且其大小由與該棧幀關聯的方法的代碼決定,另外他們的內存可以在方法被調用時再分配。
對每個線程,任意時刻都只會有一個棧幀(當前執行方法的棧幀)處于活動狀態。這個棧幀被稱為當前棧幀(current frame),相關聯的方法叫做當前方法(current method),當前方法所定義的類叫做當前類(current class)。
如果當前方法調用另一個方法,那么就會創建一個新的棧幀,并成為當前棧幀。當當前方法返回時,當前棧幀就會將返回值傳遞回前一幀,該棧幀銷毀,前一幀成為當前棧幀。
注意:某個線程創建的棧幀是該線程私有的,其他線程無法訪問到。至于詳細的方法調用和執行的過程我們在后續文章會進行更為詳細的分析。
import java.lang.reflect.Method; public class Frame { private LocalVariable[] localVariableTable;//本地變量表 private OperandStack operandStack;//操作數棧 private Method method;//常量池當前方法引用 private VMStack ownerStack;//所屬虛擬機數棧 }
四,LocalVariable (局部變量表)
上面已經提到,每個棧幀都會包含一個局部變量表(局部變量數組),用來存儲方法參數,局部變量等數據。而且局部變量表的大小由所屬棧幀的關聯方法的代碼決定,并在編譯器就確定了。
一個局部變量可以保存一個boolean,byte,char,short,int,float,reference或returnAddress的值,一對局部變量可以保存一個long或double的值。局部變量表由下標索引,索引從0開始,最大值不超過變量表大小。
long和double的值占用兩個相鄰的局部變量,而且不許用兩個局部變量中較小的那個下表來索引該long或double值。
虛擬機使用局部變量表來進行方法調用過程中的參數傳遞。在靜態方法調用時,所有的參數會按照順序保存到局部變量表中從第0個位置開始的連續的局部變量。而調用實例方法時,局部變量表的第0個位置始終保存調用該方法的對象的引用(this),然后從第1個位置開始保存方法的參數。
public class LocalVariable { private Type type; private Slot slot; public enum Type{ _boolean, _byte, _char, _short, _int, _float, _reference, _returnAddress, _long, _double } public static class Slot{ private byte[] values; } }
五,OperandStack(操作數棧)
第三部分已經提到,每個棧幀都包含一個后進先出的操作數棧,棧的深度在編譯器便已確定,其大小由與該棧幀關聯的方法體代碼決定。
JVM提供了一些將常量或局部變量表中的變量加載到操作數棧中的指令,同樣也提供了一些用來從操作數棧中獲取數據,并操作他們,然后重新放回棧中的指令。操作數棧也會被用來準備傳遞給方法的參數以及接受方法的返回值。舉個例子,iadd指令要求操作數棧頂預先有兩個int值(其他指令壓入),并將兩個值彈出棧相加,讓后將結果重新壓回棧中。
操作數棧中的每個值都可以用來存儲所有類型的數據,包括long,double。
操作數棧中的數據必須按照其類型進行適當的操作,比如我們不能將一個int值壓入棧頂后按float類型彈出并操作。
public class OperandStack { private Slot[] values; public Slot pop(){return new Slot();} public void push(Slot slot){} public static class Slot{ private byte[] v; } }
六,Method Area(方法區)
同堆內存一樣,方法區也是一個線程共享的數據區域。方法區有點類似傳統編程語言(如C)中的用來存放編譯代碼的內存區域,或者類似于操作系統進程中的文本段。它主要保存著每個類的常量池,字段,方法,以及方法或構造器中的代碼等數據(簡單理解就是,每個類的Class文件加載,解析后就被存放在方法區中了)。
方法區的生命周期與虛擬機相同。盡管虛擬機中指出邏輯上方法區是堆內存的一部分,只是垃圾回收沒有那么頻繁,但是我們習慣上都會分開來講。
public class MethodArea { private ClassInfo[] classes; public static class ClassInfo{ private RuntimeConstantPool constantPool; private Field[] fields; private Method[] methods; } }
這里我們沒有用java.lang.Class是因為我們下面要講到RuntimeConstantPool。其實方法區中存放的主要就是java.lang.Class實例集合。
七,Runtime Constant Pool(運行時常量池)
每個運行時常量池都是某個對應類或者接口的class文件中的常量池的運行時映射。一個運行時常量池就像是傳統編程語言里面的符號表,不過它所包含的數據類型比符號表豐富。
所有的運行時常量池都分配在方法區中,某個類或者接口的運行時常量池會在該類或者接口被加載時創建。
public class RuntimeConstantPool { private ClassInfo clazz; private byte[] values; }
八,PC Register(程序計數器)
或許這個該放在最前面分析的。Java的多線程機制離不開程序計數器,每個線程都有一個自己的程序計數器,以便完成不同線程上下文環境的切換。
任意時刻,如果當前方法不是native的,那么程序計數器都會保存當前被執行的指令的地址。如果當前方法是native的,那么程序計數器的值為undefined。程序計數器應該足夠大以至于可以容納returnAddress和特定平臺的指針。
public class PCRegister { private Thread ownerThread; private byte[] values; }
九,Native Method Stack (本地方法棧)
JVM的實現可以使用本地方法區來作為傳統語言的棧來支持本地方法的調用(native方法)。本地方法棧同樣可以用于其他語言(如C)寫的虛擬機指令集的解釋器實現。通常本地方法棧也是線程私有的數據區,生命周期同線程相同。
來自:http://my.oschina.net/HeliosFly/blog/357922