第一篇 JVM 之 Class文件結構
html
JVM定義了一系列程序運行期間使用的運行時數據區(run-time data area)。這些數據區域中的一些隨着JVM的啓動而建立直到JVM的中止而銷燬,而另外一些則隨着某個線程的建立而建立,隨着線程的銷燬而銷燬。java
爲了能更直觀的瞭解JVM的運行時數據區,咱們先上張圖來瞅瞅整個JVM內存的邏輯佈局:ios
以上僅是一個JVM運行時內存佈局的概念模型,咱們能夠看出JVM主要定義5大類運行時數據區:程序員
1)虛擬機棧,2)方法區,3)本地方法棧,4)堆,5)程序計數器。編程
固然除了這幾個數據區還有1)運行時常量池,2)幀,3)本地變量表,4)操做數棧等數據區,下面咱們都會一一分析。數組
對於上圖我的以爲除了看到這幾塊區域,也沒什麼深刻的細節上的信息了,並且不少狀況下還會誤導初學者,好比不少人認爲虛擬機棧就那麼一塊區域,其實否則,並且虛擬機棧能夠是不連續的。多線程
所以做爲一名程序員,我的一直認爲代碼是最好的註釋和文檔,一行代碼賽過千言萬語。所以爲了更好的理解JVM的內存模型,咱們下面用JAVA代碼的形式來深刻分析下。編程語言
零,JVM佈局
像上一篇文章同樣,咱們仍是從總體着手,而後到具體的細節逐個分析。下面就是一個可能的JVM內存的Java實現的類結構圖:this
下面咱們逐個列出每一個數據區域的類實現來(注:該實現只是一個用來幫助理解的模型,會忽略不少細節,而且可能有不正確的地方,歡迎討論)
//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 }
上述代碼很簡單,清晰明瞭,很少說了。接下來咱們就逐個深刻分析。
堆是虛擬機中線程共享的一塊數據區域,也就說全部的線程均可以訪問這塊區域的數據。同時堆是虛擬機中用來對象和數組分配內存的地方。堆的生命週期跟虛擬機同樣,在虛擬機啓動時建立,在虛擬機關閉時銷燬。另外虛擬機中的對象無需顯示的進行內存回收,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; }
相信看到代碼後你會感受更加直觀了。
JVM虛擬機棧是線程私有數據區域,也就是每一個線程都有一個本身的虛擬機棧內存,該內存隨着線程的建立而建立,隨着線程的銷燬而銷燬。虛擬機棧用來存儲棧幀(frame),而棧幀會在下文詳解。虛擬機棧相似於傳統語言(如C)中的棧。它主要用來完成方法的調用和返回。因爲虛擬機棧除了push和pop棧幀沒有其餘操做,因此虛擬機棧的內存能夠是不連續的。下面是虛擬機棧的Java代碼結構
import java.util.Stack; public class VMStack { private Thread owner; private long stackDeep;//最大棧容量 private Stack<Frame> frames; }
廢話很少說,繼續往下說,既然提到棧幀,咱們就看看什麼是棧幀。
棧幀用來存儲方法執行期間的數據和部分結果,同時還會執行動態連接,返回方法返回值,以及分派異常等動做。
每當有方法被調用時,就會建立一個新的棧幀,並壓入執行該方法的線程的虛擬機棧中。當方法執行結束後,該棧幀就會被彈出並銷燬,不管該方法是正常結束仍是異常退出。每一個棧幀內部都有一個本地變量表和操做數棧,以及一個指向當前方法所屬類的運行時常量池的引用。(本地變量表,操做數棧,運行時常量池將在下文分析)
本地變量表和操做數棧的大小在編譯期就會被肯定,而且其大小由與該棧幀關聯的方法的代碼決定,另外他們的內存能夠在方法被調用時再分配。
對每一個線程,任意時刻都只會有一個棧幀(當前執行方法的棧幀)處於活動狀態。這個棧幀被稱爲當前棧幀(current frame),相關聯的方法叫作當前方法(current method),當前方法所定義的類叫作當前類(current class)。
若是當前方法調用另外一個方法,那麼就會建立一個新的棧幀,併成爲當前棧幀。噹噹前方法返回時,當前棧幀就會將返回值傳遞迴前一幀,該棧幀銷燬,前一幀成爲當前棧幀。
注意:某個線程建立的棧幀是該線程私有的,其餘線程沒法訪問到。至於詳細的方法調用和執行的過程咱們在後續文章會進行更爲詳細的分析。
import java.lang.reflect.Method; public class Frame { private LocalVariable[] localVariableTable;//本地變量表 private OperandStack operandStack;//操做數棧 private RuntimeConstantPool constantpool;//當前方法所屬類的運行時常量池的引用 private VMStack ownerStack;//所屬虛擬機數棧 }
上面已經提到,每一個棧幀都會包含一個局部變量表(局部變量數組),用來存儲方法參數,局部變量等數據。並且局部變量表的大小由所屬棧幀的關聯方法的代碼決定,並在編譯器就肯定了。
一個局部變量能夠保存一個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; } }
第三部分已經提到,每一個棧幀都包含一個後進先出的操做數棧,棧的深度在編譯器便已肯定,其大小由與該棧幀關聯的方法體代碼決定。
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; } }
同堆內存同樣,方法區也是一個線程共享的數據區域。方法區有點相似傳統編程語言(如C)中的用來存放編譯代碼的內存區域,或者相似於操做系統進程中的文本段。它主要保存着每一個類的常量池,字段,方法,以及方法或構造器中的代碼等數據(簡單理解就是,每一個類的Class文件加載,解析後就被存放在方法區中了)。
方法區的生命週期與虛擬機相同。儘管虛擬機中指出邏輯上方法區是堆內存的一部分,只是垃圾回收沒有那麼頻繁,可是咱們習慣上都會分開來說。
更新:在HotSpot的實現中,方法區包含在了永久代中,一樣在永久代中的還有一塊區域咱們能夠稱之爲String literal pool(字符串常量池),該區域用於存放代碼中的字符串字面量,以減小相同字符串對象建立帶來的開銷。最終內存佈局參看下圖。
public class PermGen{ private MethodArea methodArea; private StringLiteralPool literalPool; } public class StringLiteralPool{ private byte[] values; } 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實例集合。
每一個運行時常量池都是某個對應類或者接口的class文件中的常量池的運行時映射。一個運行時常量池就像是傳統編程語言裏面的符號表,不過它所包含的數據類型比符號表豐富。
全部的運行時常量池都分配在方法區中,某個類或者接口的運行時常量池會在該類或者接口被加載時建立。
public class RuntimeConstantPool { private ClassInfo clazz; private byte[] values; }
或許這個該放在最前面分析的。Java的多線程機制離不開程序計數器,每一個線程都有一個本身的程序計數器,以便完成不一樣線程上下文環境的切換。
任意時刻,若是當前方法不是native的,那麼程序計數器都會保存當前被執行的指令的地址。若是當前方法是native的,那麼程序計數器的值爲undefined。程序計數器應該足夠大以致於能夠容納returnAddress和特定平臺的指針。
public class PCRegister { private Thread ownerThread; private byte[] values; }
JVM的實現可使用本地方法區來做爲傳統語言的棧來支持本地方法的調用(native方法)。本地方法棧一樣能夠用於其餘語言(如C)寫的虛擬機指令集的解釋器實現。一般本地方法棧也是線程私有的數據區,生命週期同線程相同。
更新:引用http://blog.jamesdbloom.com/JVMInternals.html 文章中的圖(很詳細)