從JAVA字節(jié)碼到JVM邏輯內(nèi)存模型


圖片來源
本文將簡單介紹java的字節(jié)碼文件以及初步探索java內(nèi)存模型,我會假定您的電腦上已經(jīng)安裝并配置好JDK,在命令行窗口下輸入java -version顯示如下信息:

D:\>java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

閱讀java字節(jié)碼文件

首先,我們從一個簡單的用例ClassDemo.java開始,類內(nèi)容如下:

public class ClassDemo {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = 1 + 2;
        int d = 3;
        System.out.println(c + d);
    }
}

ClassDemo.java文件的路徑下編譯并執(zhí)行該類,注意執(zhí)行java命令運行class文件時不需要后綴,否則報錯。如下:

D:\>javac ClassDemo.java

D:\>java ClassDemo.class
錯誤: 找不到或無法加載主類 ClassDemo.class

D:\>java ClassDemo
6

編譯后的文件ClassDemo.class文件用UE直接打開內(nèi)容如下,文件開頭是一個0xcafebabe```16進制特殊標志(魔數(shù)),文件內(nèi)容我們無法閱讀:

直接打開.class文件

我們需要對class文件進行反編譯,使用javap -v命令將class文件反編譯并輸出到ClassDemo.txt中:

D:\>javap -v ClassDemo.class > ClassDemo.txt

打開ClassDemo.txt就可以看到反編譯后的class文件指令集:

Classfile /D:/ClassDemo.class
  Last modified 2018-12-28; size 416 bytes
  MD5 checksum 8fc0289dea72a11671d7a74bdb62a225
  Compiled from "ClassDemo.java"
public class ClassDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // ClassDemo
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               ClassDemo.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               ClassDemo
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{
  public ClassDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iconst_3
         5: istore_3
         6: iconst_3
         7: istore        4
         9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: iload_3
        13: iload         4
        15: iadd
        16: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        19: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 6
        line 7: 9
        line 8: 19
}
SourceFile: "ClassDemo.java"

class文件是交給JVM閱讀并解釋執(zhí)行的,其中包括:java版本、訪問標志、常量池、當前類、超類、接口、字段、方法、屬性。

JVM邏輯內(nèi)存模型

接下來我們先看看JVM中內(nèi)存的主要邏輯劃分,然后結(jié)合class字節(jié)碼文件理解JVM各內(nèi)存區(qū)域存儲的具體內(nèi)容。

jvm邏輯內(nèi)存模型

線程共享內(nèi)存區(qū)域

所有線程都能訪問這塊內(nèi)存空間,隨虛擬機或者GC而創(chuàng)建和銷毀。

  • 方法區(qū)(Method Area,非堆Non-Heap):JVM用來存儲已加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),虛擬機規(guī)范中這是一個邏輯區(qū)域。具體實現(xiàn)根據(jù)不同虛擬機來實現(xiàn),如:oracle的HotSpot在java7中方法區(qū)放在永久代,java8放在元數(shù)據(jù)空間,并且通過GC機制對這個區(qū)域進行管理,回收主要目標是針對常量池的回收和對類型的卸載。

運行時常量池(Runtime Constant Pool)是方法區(qū)的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是:String類的intern()方法。

  • 堆內(nèi)存(也稱"GC"堆,Garbage Collected Heap):所有的對象實例以及數(shù)組都要在堆上分配,但是隨著JIT 編譯器的發(fā)展與逃逸分析技術(shù)的逐漸成熟,棧上分配、標量替換優(yōu)化技術(shù)將會導致一些微妙的變化發(fā)生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。由jvm自動垃圾回收器來管理。

線程私有內(nèi)存區(qū)域

每個線程都會有獨立的內(nèi)存空間,隨線程生命周期而創(chuàng)建和銷毀。

  • 虛擬機棧(Java Virtual Machine Stacks):線程棧由多個棧幀(Stack Frame)組成。一個線程會執(zhí)行一個或多個方法,一個方法對應一個棧幀。棧幀內(nèi)容包括:局部變量表、操作棧、動態(tài)鏈接、方法返回地址、附加信息等。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
    如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError 異常;如果虛擬機??梢詣討B(tài)擴展,當擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError 異常。

局部變量包括:基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)、對象引用reference類型和returnAddress類型。

  • 程序計數(shù)器(Program Counter Register):它的作用可以看做是當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復等功能都依賴于此。
    每個線程都有一個私有的程序計數(shù)器空間,占用很少的內(nèi)存空間。
    執(zhí)行java方法時,計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;如果執(zhí)行Native方法,則計數(shù)器值為空(Undefined)。
    CPU同一時間,只會執(zhí)行一條線程中的指令。JVM會在多線程間輪流切換并使用CPU分配的執(zhí)行時間。線程切換后,需要通過程序計數(shù)器來恢復正確的執(zhí)行位置。
    此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
  • 本地方法棧(Native Method Stacks);與虛擬機棧作用類似,服務(wù)于Native方法,同時也會拋出:StackOverflowError和OutOfMemoryError異常。

字節(jié)碼文件分析

接下來,我們分析一下反編譯的ClassDemo.txt文件內(nèi)容的具體含義。

字節(jié)碼中的類信息

public class ClassDemo
  minor version: 0                                      //次版本號
  major version: 52                                     //主版本號
  flags: ACC_PUBLIC, ACC_SUPER                          //訪問標志

版本號規(guī)則:

JDK版本 字節(jié)碼中的主版本號
JDK1.2 0x002E = 46
JDK1.3 0x002F = 47
JDK1.4 0x0030 = 48
JDK5 0x0031 = 49
JDK6 0x0032 = 50
JDK7 0x0033 = 51
JDK8 0x0034 = 52

訪問標志規(guī)則:


訪問標志

字節(jié)碼中的常量池信息

Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16      // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // ClassDemo
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               ClassDemo.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               ClassDemo
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V

常量池規(guī)則

字節(jié)碼中<init>表示構(gòu)造函數(shù),而()V的解釋如下:

引用】在JVM規(guī)范中,每個變量/字段都有描述信息,描述信息主要的作用是是描述字段的數(shù)據(jù)類型、方法的參數(shù)信息(包括數(shù)量、類型與順序)與返回值,根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型和代表無返回值的void類型都用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示,為了壓縮字節(jié)碼文件的體積,對于基本數(shù)據(jù)類型,JVM都只使用一個大寫字母來表示,如下所示:B -> byte、C -> char、D -> double、F -> float、I -> int、J -> long【由于L被其它數(shù)據(jù)類型給占用了所以用J來表示】、S -> short、Z -> boolean【由于B已經(jīng)被前面的byte類型所占用】、V -> void、 L -> 對象類型,如Ljava/lang/String;
對于數(shù)組類型來說,每一個維度使用一個前置的“[”來表示,如int[]被記錄為[I、String[][]被記錄為[[Ljava/lang/String
用描述符描述方法時,按照先參數(shù)列表,后返回值的順序來描述。參數(shù)列表按照參數(shù)的嚴格順序放在一組()之內(nèi),如方法:
String getRealNamebyIdAndNickName(int id, String name)的描述符為:(I, Ljava/lang/String) Ljava/lang/String;

字節(jié)碼中的構(gòu)造函數(shù)信息

public ClassDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

由于沒有顯示的申明構(gòu)造函數(shù),此處的默認的無參構(gòu)造函數(shù)。
descrptor:方法入?yún)⒑头祷孛枋?br> flags:訪問控制。
stack:方法對應棧幀中的操作數(shù)棧的深度
locals:本地變量數(shù)量
args_size:參數(shù)數(shù)量

其中無參構(gòu)造器但是args_size=1是因為無參構(gòu)造器和非靜態(tài)方法調(diào)用會默認傳入this變量參數(shù),其中aload_0即表示的this,stack=1,locals=1同理。

invokespecial:調(diào)用一個初始化方法,私有方法或者父類的方法。
invokestatic:調(diào)用靜態(tài)方法
invokevirtual:調(diào)用實例方法

引用】LineNumberTable:為調(diào)試器提供源碼中的每一行對應的字節(jié)碼信息。即源碼與指令集的對應關(guān)系。

字節(jié)碼中的方法信息

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iconst_3
         5: istore_3
         6: iconst_3
         7: istore        4
         9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        12: iload_3
        13: iload         4
        15: iadd
        16: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        19: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 6
        line 7: 9
        line 8: 19

內(nèi)存溢出示例

下面是三個內(nèi)存溢出和堆棧溢出的示例,增加對內(nèi)存模型的理解。

  • 代碼
import java.util.List;
import java.util.ArrayList;

public class HeapOOMTest {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<Object>();
        while(true) {
            list.add(new Object());
        }
    }
}
  • 輸入輸出:
D:\>javac HeapOOMTest.java

D:\>java -Xmx10M -Xms10M HeapOOMTest
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.ArrayList.grow(Unknown Source)
        at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
        at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
        at java.util.ArrayList.add(Unknown Source)
        at HeapOOMTest.main(HeapOOMTest.java:8)
  • 代碼
public class StackOverflowTest {
    public static void main(String[] args) {
        recursion();
    }
    
    public static void recursion() {
        recursion();    
    }
}
  • 輸入輸出
D:\>javac StackOverflowTest.java

D:\>java -Xmx10M -Xms10M StackOverflowTest
Exception in thread "main" java.lang.StackOverflowError
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
        at StackOverflowTest.recursion(StackOverflowTest.java:8)
  • 代碼
import java.util.List;
import java.util.ArrayList;

public class ConstantPoolTest {
    
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true) {
            list.add(String.valueOf(i++).intern()); 
        }
    }

}
  • 輸入輸出
D:\>javac ConstantPoolTest.java

D:\>java -Xmx10M -Xms10M ConstantPoolTest
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.lang.Integer.toString(Unknown Source)
        at java.lang.String.valueOf(Unknown Source)
        at ConstantPoolTest.main(ConstantPoolTest.java:10)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容