JVM學習——解析Java虛擬機運行時數據區

JVM學習——解析Java虛擬機運行時數據區

1 運行時數據區概覽

來一張圖大概看一下JVM運行時數據區的情況,下面我將仔細的介紹各個區域

2 運行時數據區

2.1 程序計數器

程序計數器是一塊較小的內存空間,可以看作是當前線程(每個線程都有自己的程序計數器)所執行的字節碼的行號指示器。字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

如果線程執行的是一個java方法,那麼這個線程記錄的就是正在執行的虛擬機字節碼指令的地址。如果是Native方法,那麼計數器爲空(Undefined)。

2.2 Java虛擬機棧

Java虛擬機棧也是線程私有的,生命週期與線程相同。具體結構如圖所示:

2.2.1 局部變量表

存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和remoteAddress類型(指向了一個字節碼指令的地址)

其中64位的long和double類型的數據會佔用2個局部變量 空間(slot),其餘的數據類型只佔用一個空間

局部變量表所需要的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

2.2.2 操作數棧

用於在方法運行時可以存放以及獲取操作數(比如一個加法指令,需要將兩個數據壓入棧中,進行加法操作後又需要將結果壓入操作數棧)。其所需要的最大棧深度也是在編譯期間定下的,在方法剛開始運行的時候,操作數棧是空的。

2.2.3 動態鏈接

方法區的常量池中會存儲方法的符號引用,每個棧幀中都會存儲一個引用,用於指向常量池中該方法對應的符號引用,字節碼指令中方法的調用就是通過符號引用來進行的。在類加載階段的解析階段,部分符號引用會被解析爲直接引用,稱爲靜態解析(條件爲在調用前就有一個可確定的調用版本)。在方法運行過程中,另一部分的符號引用會被實時解析成直接引用,稱爲動態鏈接

2.2.4 返回地址

方法的運行過程中,可能會正常退出,也可能會異常退出,不論是哪種退出方式,在退出後都會要保證其上層調用者可以知道方法退出的位置,以便於程序繼續執行,方法的返回地址就是用於確定退出位置的。

2.3 本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,他們的區別在於虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。(HotSpot中將本地方方法棧與虛擬機棧合二爲一)

2.4 Java堆

Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。對象實例與數組都要在堆上分配(也不是那麼絕對了。逃逸分析)。是垃圾蒐集器管理的主要區域。

2.5 方法區

方法區也是一個線程共享的內存區域,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

2.5.1 運行時常量池

運行時常量池(相對於Class文件常量池的另外一個重要特徵是具備動態性,並不一定是編譯期才能產生,String的intern()方法)是方法區的一部分。

class文件中除了有類的版本、字段、方法、接口等描述信息,還有一項信息也是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

3 從JVM角度思考類的各種變量的存放位置

3.1 成員變量

3.1.1 類變量(static修飾)

在類加載的過程中的準備階段

正式爲類變量分配內存並設置類變量的初始值

這些變量所使用的內存都將在方法區進行分配

3.1.2 實例變量

實例變量將在對象實例化時隨着對象一起分配到Java堆上

3.2 局部變量

存放在Java棧幀中的局部變量表中,每個棧幀中的局部變量可能不同。JVM通過索引的方式來訪問變量表中的變量。

3.3 一些思考

3.3.1 靜態方法與非靜態方法

在學習Java反射的時候存在着這樣一個例子

public class A {

    public static void staticMethod() {
        System.out.println("我是靜態方法");
    }

    public void normalMethod() {
        System.out.println("我是一般方法");
    }

}
public class ReflectTest {

    public static void main(String[] args) throws Throwable {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class<?> aClass = classLoader.loadClass("model.A");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
        A a = (A) declaredConstructor.newInstance();

        Method setA = aClass.getMethod("setId",int.class);

        Method staticMethod = aClass.getMethod("staticMethod");
        
        //未傳入對象都可以調用靜態方法
        staticMethod.invoke(null);

        Method normalMethod = aClass.getMethod("normalMethod");
        
        //必須明確指定調用哪個對象的非靜態方法
        normalMethod.invoke(a);
    }
}

那出現這種狀況的原因是什麼呢?非靜態方法與靜態方法有一個很重大的不同:

非靜態方法有一個隱含的傳入參數,該參數是JVM給它的,這個參數是什麼呢?參數具體就是指向對象實例的,存儲在棧上的指向實例的地址。有了這個地址,非靜態方法才能找到在堆上實例對象中的實例變量的值。所以在調用非靜態方法時,必須指定實例。

靜態方法不需要這個參數,它是屬於類的,而不是屬於某個對象的,在加載類時,就會爲靜態方法分配內存,不依賴於對象,先於對象存在,直接通過類名.靜態方法及可訪問。

3.3.2 this

this不能訪問類變量。this指向的是對象,在Java堆中的對象有實例變量,所以可以用this來指向相應的變量。但是類變量存放在方法區,所以this不能找到類變量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章