JVM內存模型、垃圾回收、字節碼基礎

前言

java文件被jvm編譯成.class文件,.class文件中全部是二進制的數據。在JVM中用一個8bit的變量類型存儲指令,這樣0到255可以表示總共256個指令。我們編寫的代碼被編譯成相應的指令碼交給計算機執行,而在對代碼進行調優的時候,可以讀懂編譯後的.class文件也是很重要的。

基礎知識

Java內存模型

這裏寫圖片描述
首先要先了解一下Java的內存模型,我們比較關心的主要有四個部分:堆,虛擬機棧,本地方法棧,方法區。
堆一般存放對象實例,也就是我們new 分配的對象,堆中依據對象存活時間分爲新生代和老年代,新生代中又分爲Eden空間和Survivor空間(主要是執行復制回收算法時使用的空間),新生代的Survivor空間又分成from空間和to空間。這裏簡單介紹一下新生代中發生垃圾回收的過程:
這裏寫圖片描述
新生代生成對象的時候,首先分配在Eden區,當Eden區滿了以後,執行復制清除算法,將Eden區的對象複製到From然後清除Edne區。這樣下一輪時,可以繼續在Eden區中分配,Eden和From區是目前存活的對象,當Eden再次滿了以後,下一次MinorGC(針對新生代的GC,MajorGC針對老年代,FullGC針對全部)會將Eden和From複製到To中並清除原來的內容,以此類推。如果存活的對象太多導致Survivor區域無法容納,還需要老年代進行分配擔保,將無法保存在Survivor中的對象直接晉升到老年代。
方法區主要存放一些靜態變量,類信息等,通常我們可以把這部分看作永久代,因爲其中分配的對象不會被垃圾收集。
運行時常量池是class文件中每一個類或者接口的常量表,包含了字面量和符號飲用,充當一個符號表的作用。比如Java中的字符串,默認聲明其實作爲字面量存儲在常量池中的,參考如下代碼:

        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a == b);
        System.out.println(a == c);

代碼中,a==b返回true,a==c返回false,這是因爲默認a和b的聲明方式實際是在常量池中聲明一個符號,然後a和b都指向那個符號”abc”,而c是在堆中聲明瞭一個char數組。java中==默認判斷的是兩個值地址是否相等,所以第一個返回true,第二個返回false。
前面提到,常量池相當於一個符號表。我們可以把它看作一個表結構,鍵是常量地址,值就是存儲的值。如下:
這裏寫圖片描述

而符號引用,其實就是指類和接口的全限定名和方法的描述符,熟悉jni的話會比較清楚這些東西。簡單來講,在一個實例中可能有另一個對象的引用,那麼在class文件中其實存放的是符號引用,也就是java/lang/object 這種字符串,而不是真正的指向內存地址的引用。在動態鏈接的過程,纔會把class文件中這些“假的”符號飲用轉換成真正的直接引用(也就是指向一個內存地址)。
本地方法棧就是通過Jni調用底層方法的時候,本地C/C++代碼執行時候的方法棧。
虛擬機棧就是我們的Java代碼執行的棧了,也是本文的重點。

Java虛擬機棧

棧幀

當一個方法被調用的時候,就進入它所在的方法棧,棧幀隨着方法的創建而創建,隨着方法的結束而銷燬。每一個棧幀都擁有自己的本地變量表,操作數棧和運行時常量池的引用。

局部變量表

局部變量表中用slot來進行存儲,一個slot可以存放一個boolean、byte、char、short、int、float、reference或returnAddress的數據,兩個slot可以存儲一個long或double。

操作數棧

每個棧幀中有一個操作數棧,用來存放指令執行的中間結果。有點類似於一個棧實現的計算器。
比如當我們執行一個iadd指令中,則要求操作數棧頂是兩個int類型的數值,執行iadd後會把兩個數值取出來求和再把結果放回操作數棧中。

基本指令集

.class文件是二進制文件,嚴格規定了每個字節的含義。是一組以8位字節爲基礎單位的二進制流,所以又叫字節碼。只有兩種數據類型:無符號數和表。
無符號數可以存放數字、索引引用或者UTF-8編碼構成的字符串值。
表是無符號數和其他表構成的符合數據類型。指令碼由一個字節表示,不同的數字0到200多代表不同的指令。
當然在分析的時候,我們一般使用javap -verbose 命令對class文件進行反編譯,可以得到相應的明文指令,避免了我們對字節碼參照JVM規範手冊人工去翻譯。所以我們主要關注的是一些指令的具體含義。

加載和存儲指令

  • 將一個本地變量加載到操作數棧:load相關指令,比如iload加載int類型,fload加載float類型等
  • 將數值從操作數棧加載到本地變量表:store相關指令
  • 加載常量到操作數棧:push、const相關指令
    對於一些指令,比如iload_1,iload_2就是將操作數隱藏在指令中,就等同於iload 1,iload 2.
    此外,需要說明,iconst n是把常量n壓入操作數棧,istore n 是把操作數棧頂的數存在本地變量表第n個位置,iload n是把本地變量表第n個元素壓入操作數棧,本地變量表可以看作一個ArrayList鏈表數據結構。
    這裏寫圖片描述

可以看到在棧幀中,對於變量的操作流程,基本就是把值從常量池拿到操作數棧,要存儲的話就放在本地變量表,要計算了再拿到操作數棧,然後調用相應的指令進行計算。

算數指令

算數指令用於兩個操作數棧上的值進行特定運算,並把計算結果壓入操作數棧。比如add,sub,mul,div相關。

方法調用和返回指令

invokevirtual用於調用實例方法,invokespecial用於調用一些特殊實例方法,比如構造方法,invokestatic用於調用靜態方法。
返回指令即return相關,比如ireturn。

其餘指令

其餘指令包括類型轉換指令,對象創建與操作指令,操作數棧管理指令和控制轉移指令,在這裏不詳細介紹可以查閱Java虛擬機規範。

實例

下面介紹一個工程實例,Java代碼如下:

public class Main {
    public int cal() {
        int a = 1;
        int b = 1;
        return a + b;
    }
   public int getInteger() {
        Random random = new Random();
        return random.nextInt(5);
    }
}

利用javap -verbose命令對class文件進行反編譯,查看字節碼:
反編譯後,生成的字節碼如下:

public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = Class              #25            // java/util/Random
   #3 = Methodref          #2.#24         // java/util/Random."<init>":()V
   #4 = Methodref          #2.#26         // java/util/Random.nextInt:(I)I
   #5 = Class              #27            // Main
   #6 = Class              #28            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LMain;
  #14 = Utf8               cal
  #15 = Utf8               ()I
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               b
  #19 = Utf8               getInteger
  #20 = Utf8               random
  #21 = Utf8               Ljava/util/Random;
  #22 = Utf8               SourceFile
  #23 = Utf8               Main.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               java/util/Random
  #26 = NameAndType        #29:#30        // nextInt:(I)I
  #27 = Utf8               Main
  #28 = Utf8               java/lang/Object
  #29 = Utf8               nextInt
  #30 = Utf8               (I)I
{
  public Main();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMain;

  public int cal();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   LMain;
            2       6     1     a   I
            4       4     2     b   I

  public int getInteger();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/util/Random
         3: dup
         4: invokespecial #3                  // Method java/util/Random."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_5
        10: invokevirtual #4                  // Method java/util/Random.nextInt:(I)I
        13: ireturn
      LineNumberTable:
        line 12: 0
        line 13: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LMain;
            8       6     1 random   Ljava/util/Random;
}
SourceFile: "Main.java"

下面開始分析主要部分:

運行時常量池

Constant pool 就是前面提到的運行時常量池,類似一個符號表,前面表示在常量池中的地址,後面的字段就是相應的值,同時Javap還會幫助我們生成一些字段輔助查看:
#1 = Methodref #6.#24 // java/lang/Object.””:()V
#2 = Class #25 // java/util/Random
#25 = Utf8 java/util/Random
#26 = NameAndType #29:#30 // nextInt:(I)I

在我們class文件的常量池中有四種類型的常量,Utf8就是字面常量,一個字符串。而Methodref是一個方法的符號引用,爲什麼說是符號引用呢,就是因爲最後看到它其實就是一個方法描述符:字符串而已。同時,Class表示引用到的一個類,NameAndType表示一個字段或者方法。

Code屬性

下面是方法的描述符標誌位等信息,然後是最重要的信息:code屬性。
分析其中的cal方法:

public int cal();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
      ...
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   LMain;
            2       6     1     a   I
            4       4     2     b   I

stack = 2表明操作數棧最大深度是2,locals=3是本地變量表最大數目爲3,args_size = 1,是因爲該方法是實例方法,所以默認有一個輸入參數this,指向方法所在的實例。
下面的LineNumberTable表示java源代碼和字節碼的對應關係,用於堆棧跟蹤。
LocalVariableTable描述局部變量表中的變量和Java源代碼定義變量的對應關係。其中start代表局部變量聲明開始,length表示在字節碼中存活的長度,結合起來就是作用域範圍。參考下面的指令碼:this在一開始就聲明,時間限定爲0,變量a在第二個時刻 istore_1存入局部變量表,所以start的值就是2,b在第四個時刻存入,所以start是4,聲明週期可以依次推理。

         0: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn

再來簡單分析一下指令,結合前面提高的指令介紹:
iconst_1,將常數1加載到操作數棧,istore_1將1從操作數棧棧頂元素1存儲到局部變量表第一個位置,下面的2,3條指令同理。iload_1把局部變量表第一個元素加入操作數棧,iload_2同理,現在操作數棧有兩個元素 1、1,然後iadd取出棧頂兩個元素相加,ireturn返回。

總結

基本內容就是這些,對於字節碼的學習,更詳細的內容可以參考Java虛擬機規範。

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