深刻Java虛擬機(三)Java類型的生命週期

上一篇簡單記錄了Java class文件的格式,它以標準的二進制形式來表現Java類型。本篇咱們來看下當二進制的類型數據被導入到和Java虛擬機中時,到底會發生什麼。咱們以一個Java類型(類或接口)的生命週期(從進入虛擬機開始到最終退出)爲例來討論開始階段的裝載、鏈接和初始化,以及佔Java類型生命週期絕大部分時間的對象實例化、垃圾收集和對象finalize,而後是Java類型生命週期的結束(從虛擬機中卸載)java

生命週期的開始--類型裝載、鏈接與初始化

Java虛擬機經過裝載鏈接初始化一個Java類型,使該類型能夠被正在運行的Java程序所使用。網絡

  • 裝載---就是把二進制形式的Java類型讀入Java虛擬機中
  • 鏈接---就是把這種已經讀入虛擬機的二進制形式的類型數據合併到虛擬機的運行時狀態中去。鏈接分爲三個子步驟:
    • 驗證---確保Java類型數據格式正確,而且適於Java虛擬機的使用。
    • 準備---負責爲該類型分配所須要的內存,好比爲它的類變量分配內存。
    • 解析---負責把常量池中的符號引用轉換爲直接引用。不過虛擬機的實現能夠推遲這一步,它能夠在程序真正使用某個符號引用時再去解析它。
  • 初始化---給類變量賦予正確的初始值。

總體流程以下:數據結構

image

如圖所示,裝載、鏈接和初始化這三個階段必須按順序進行。惟一例外的就是鏈接階段的第三步(解析),它能夠在初始化以後再進行。app

在類和接口被裝載和鏈接的時機上,Java虛擬機規範對具體實現提供了必定的靈活性。可是規範對於初始化的時機有着嚴格的規定。全部Java虛擬機實現必須在每一個類或接口首次主動使用時初始化。下面6種情形符合主動使用的情形。dom

  • 當建立某個類的新實例時(經過執行new指令;或者不明確的建立、反射、克隆或者反序列化)
  • 當調用某個類的靜態方法時(即在字節碼中執行invokestatic指令輸入時)。
  • 當使用某個類或接口的靜態字段,或者對該字段賦值時(即在字節碼中執行getstatic或putstatic指令時),用final修飾的靜態字段除外,它被初始化爲一個編譯時的常量表達式。
  • 當調用JavaAPI中的某些反射方法時,好比類Class中的方法或者java.lang.reflect包中類的方法。
  • 當初始化某個類的子類時(某個類初始化時,要求它的超類已經被初始化了)
  • 當虛擬機啓動時某個被標明爲啓動類的類(即含有main()方法的那個類)

除以上6種情形外,全部其餘使用 Java 類型的方式都是被動使用,它們都不會致使 Java 類型的初始化。jvm

第五條中任何一個類的初始化都要求它的超類在此以前完成初始化。然而對於接口來講,這條規則並不適用。只有在某個接口所聲明的非final字段被使用時,該接口才會被初始化。函數

首次主動使用時初始化這個規則直接影響着類的裝載、鏈接和初始化的機制。虛擬機實現能夠自由選擇裝載、鏈接的時機。但不管如何,若是一個類型在它首次主動使用以前尚未被裝載和鏈接的話,那它必須在此時被裝載和鏈接,這樣它才能被初始化。性能

裝載

裝載動做由三個基本動做組成,要裝載一個類型,Java虛擬機必須:學習

  • 經過該類型的徹底限定名,產生一個表明該類型的二進制數據流。
  • 將二進制數據流解析爲方法區內的內部數據結構。
  • 建立一個表示該類型的java.lang.Class類的實例

Java虛擬機並無說Java類型的二進制數據應該怎樣產生。因此咱們能夠想象這幾種場景:ui

  • 本地文件系統裝載class文件
  • 網絡下載class文件
  • 把一個源文件動態編譯爲class文件

有了二進制數據後,Java虛擬機必須對這些數據進行足夠的處理,而後才能建立類java.lang.Class的實例對象。而裝載步驟的最終產品就是這個Class類的實例對象,它成爲Java程序與內部數據結構之間的接口。要訪問關於該類型的信息,程序就要調用該類型對應的Class實例對象的方法。

前面講過Java類型的裝載要麼由啓動類裝載器裝載,要麼由用戶自定義的類裝載器裝載。而在裝載過程當中遇到問題,類裝載器應該在程序首次使用時報告問題。若是這個類一直沒有被程序主動使用,那麼該類裝載器不該該報告錯誤。

鏈接-第一階段:驗證

當類型被裝載後,就準備進行鏈接了。鏈接過程的第一步是驗證:確認類型符合Java語言的語義,而且不會危及虛擬機的完整性。

在驗證上,不一樣虛擬機的實現可能不太同樣。但虛擬機規範列出了虛擬機能夠拋出的異常以及在何種條件下必須拋出它們。

驗證階段前的--驗證

而在裝載過程當中,也可能會作如下幾種數據檢查(雖然這些檢查在裝載期間完成,在正式的鏈接驗證階段以前進行,但在邏輯上屬於驗證階段。檢查被裝載類型是否有任何問題的過程都屬於驗證):

  • 在解析二進制流並轉化爲內部數據結構的時候,虛擬機要確保數據所有是預期格式。可能檢查魔數,確保每個部分都在正確的位置,是否擁有正確的長度等等。
  • 另外一個可能在裝載過程時進行的檢查使,確保除了Object以外的每個類都有一個超類。

驗證階段後的--驗證

在大多數虛擬機的實現中,還有一種檢查每每發生在正式的驗證階段以後,那就是符號引用的驗證。

前面講過動態鏈接的過程包括經過保存在常量池彙總的符號引用查找被引用的類、接口、字段以及方法,把符號引用替換爲直接引用。

當虛擬機搜尋一個被符號引用的元素(類型、方法)時,它必須首先確認該元素存在。若是該元素存在,還要進一步檢查引用類型是否有訪問該元素的權限。這些存在性和訪問權限的檢查在邏輯上屬於鏈接的第一階段,可是每每在鏈接的第三階段解析的時候發生。而解析自身也可能延遲到符號引用第一次被程序使用的時候,因此這些檢查設置可能在初始化以後才進行。

正式的驗證階段

任何在此以前沒有進行的檢查以及在此以後不會被檢查的項目都包含在內。

  • 檢查 final 的類不能擁有子類
  • 檢查 final 的方法不能被覆蓋
  • 確保在類型和其父類之間沒有不兼容的方法聲明

請注意,當須要查看其餘類型時,它只須要查看超類型。超類須要在子類初始化前被初始化,因此這些類應該已經被裝載了。而對於接口的初始化來講,不須要父接口的初始化,可是當子接口被裝載時,父接口須要被裝載(它們不會被初始化,只是被裝載了,有些虛擬機實現也可能會進行鏈接的操做)

  • 檢查全部的常量池入口之間的一致性(好比,一個CONSTANT_String_info入口的 string_index項目必須是CONSTANT_Utf8_info入口的索引)
  • 檢查常量池中全部的特殊字符串(類名、字段名和方法名、字段描述符和方法描述符)是否符合格式。
  • 檢查字節碼的完整性。

全部的Java虛擬機都必須設法爲它們執行的每一個方法檢驗字節碼的完整性。好比,不能由於超出了方法末尾的跳轉指令而致使虛擬機的崩潰,虛擬機必須在字節碼驗證的時候檢查出這樣的跳轉指令是非法的,從而拋出一個錯誤。

虛擬機的實現並無強求在正式的鏈接驗證階段進行字節碼驗證,因此虛擬機能夠選擇在執行每條語句的時候單獨進行驗證。然而Java虛擬機指令集設計的一個目標就是使得字節碼流可使用一個數據流分析器一次驗證,而不用在程序執行時動態驗證,對速度的提高有很大幫助。

鏈接-第二階段:準備

隨着Java虛擬機裝載了一個類,並執行了一些它選擇進行的驗證後,類就能夠進入準備階段了。在準備階段,Java 虛擬機爲類變量分配內存,設置默認初始值。但在達到初始化以前,類變量都沒有被設置爲真正的初始值(代碼裏聲明的)。準備階段是不會執行 Java 代碼的

在準備階段,虛擬機給類變量的默認初始值以下表(有木有感受跟 C 的默認數據類型很像)

類型 默認初始值
int 0
long 0L
short 0
char "\u0000"
byte 0
boolean false
reference null
float 0.0f
double 0.0d

Java 虛擬機一般把 boolean 實現爲一個 int,會被默認賦值爲0(對應 false)

在準備階段,Java 虛擬機實現可能也爲一些數據結構分配內存,目的是爲了提升運行程序的性能。這種數據結構好比方法表,它包含指向類中每個方法(包括從父類繼承的方法)的指針。方法表能夠在執行繼承的方法時不須要搜索父類。

鏈接-第三階段:解析

Java 類型通過驗證和準備以後,就能夠進入解析階段了。解析的過程就是在類型的常量池中尋找類、接口、字段和方法的符號引用,並把這些符號引用轉換成直接引用的過程。

本篇要從宏觀角度看待生命週期,解析過程先簡單描述下,後面作詳細介紹

初始化

爲了準備讓一個類或接口被首次主動使用,最後一個步驟就是初始化。初始化就是爲類變量賦予正確的初始值(就是代碼裏指定的數值)。

在 Java 代碼中,一個正確的初始值是經過類變量初始化語句或者靜態初始化語句給出的。
類變量初始化語句(組成:=、表達式):

class ExampleA{
    static int size = (int) (3 * Math.random());
}
複製代碼

靜態初始化語句(組成:static 代碼塊):

class ExampleB{
    static int size ;
    static {
        size = (int) (3 * Math.random());
    }
}
複製代碼

全部的類變量初始化語句和類型的靜態初始化器都被 Java 編譯器收集在一塊兒,放到一個特殊的方法中。在類和接口的 class 文件中,這個方法被稱爲<clinit>。一般的 Java 程序方法是沒法調用這個<clinit>方法的。這種方法只能被虛擬機調用,專門用來把類型的靜態變量設置爲它們的正確值。

初始化一個類包含兩個步驟:

  • 若是類存在直接超類的話,而且超類尚未被初始化,就先初始化直接超類
  • 若是類存在一個類初始化方法,就執行此方法

而接口的初始化並不須要初始化它的父接口;若是接口存在一個初始化方法的話,就執行此方法。

<clinit>方法的代碼並不會顯式調用超類的<clinit>。在Java虛擬機調用類的<clinit>方法以前,它必須確認超類的<clinit>方法已經被執行了。

<clinit>方法

前面講到Java 編譯器把類變量初始化語句和靜態初始化代碼塊都放到 class 文件的<clinit>方法中,順序就按照在類或接口中聲明的順序。

以下類:

class ExampleB{
    static int width ;
    static int height = 4 * ExampleA.random();
    static {
        width = 9 * ExampleA.random() + 10;
    }
}
複製代碼

Java 編譯器生成了以下<clinit>方法

static <clinit>()V
   L0
    LINENUMBER 14 L0
    ICONST_4
    INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
    IMUL
    PUTSTATIC hua/lee/jvm/ExampleB.height : I
   L1
    LINENUMBER 16 L1
    BIPUSH 9
    INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
    IMUL
    BIPUSH 10
    IADD
    PUTSTATIC hua/lee/jvm/ExampleB.width : I
   L2
    LINENUMBER 17 L2
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
複製代碼

並不是全部的類編譯後都存在<clinit>方法:

  • 若是類沒有聲明任何類變量,也沒有靜態初始化語句,那麼它就不會有<clinit>方法。
  • 若是類聲明瞭類變量,可是沒有明確的進行初始化,也不會有<clinit>方法。
  • 若是類僅包含靜態 final 變量的類變量初始化語句,並且初始化語句採用 編譯時常量表達式,類也不會有<clinit>方法。

下面的類是不會執行<clinit>方法的。

class ExampleB{
    static final int angle = 10;
    static final int length = angle * 2;
}
複製代碼

ExampleB 聲明瞭兩個常量anglelength,並經過表達式賦予了初始值,這些表達式是編譯時常量。編譯器知道angle表示十、length表示20。因此在ExampleB被裝載時,anglelength並無做爲類變量保存在方法區中,它們是常量,被編譯器特殊處理了。

anglelength做爲常量,Java 虛擬機在使用它們的全部類的常量池或者字節碼流中直接存放的是它們表示的int 數值。好比,若是一個類A使用了 Example 的angle字段,在編譯時虛擬機不會在類 A的常量池中保存一個指向Example類 angle 的符號引用,而是直接在類 A 的字節碼流中嵌入一個值10。若是angle的常量值超過了 short 範圍限制,好比 angle=40000,那麼類會將它保存在常量池的 CONSTANT_Integer中,值爲40000。

而對於接口來講,咱們能夠關注下面這個代碼

interface ExampleI{
    int ketch = 9;
    int mus = (int) (Math.random()*10);
}
複製代碼

編譯後的<clinit> 方法爲

static <clinit>()V
   L0
    LINENUMBER 22 L0
    INVOKESTATIC java/lang/Math.random ()D
    LDC 10.0
    DMUL
    D2I
    PUTSTATIC hua/lee/jvm/ExampleI.mus : I
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0
複製代碼

請注意,只有mus<clinit>初始化了。由於ketch字段被初始化爲了一個編譯時常量,被編譯器特殊處理了。和類中的邏輯同樣,對於其餘引用到mus的類,編譯器會保存指向這個字段的符號引用;而對於引用ketch的類,會在編譯時替換爲常量值。

主動使用被動使用

前面說過,Java 虛擬機在首次使用類型時初始化它們。只有6種活動被認爲是主動使用:

  • 建立類的新實例
  • 調用類中聲明的靜態方法
  • 操做類或接口中聲明的非 final 靜態字段
  • 調用 API 種特定的反射方法
  • 初始化一個類的子類
  • 指定一個類做爲Java 虛擬機的啓動入口

使用一個非 final 的靜態字段只有當類或者接口明確聲明了這個字段時纔是主動使用。好比,父類種聲明的字段可能會被子類引用;接口中聲明的字段可能會被實現者引用。對於子類、子接口和接口實現類來講,這就是被動使用。而被動調用並不會觸發子類(調用字段的類)的初始化。

示例:

class NewParent{
    static int hoursOfSleep = (int) (Math.random() * 3);
    static{
        System.out.println("NewParent was initialized.");
    }
}

class NewKid extends NewParent{
    static int hoursOfCrying = 6+(int) (Math.random() * 2);
    static{
        System.out.println("NewKid was initialized.");
    }
}
public class Test {
    public static void main(String[] args) {
        int h = NewKid.hoursOfSleep;
        System.out.println(h);
    }
    static{
        System.out.println("MethodTest was initialized.");
    }
}
複製代碼

輸出以下:

MethodTest was initialized.
NewParent was initialized.
2
複製代碼

從log看,執行Testmain方法只會致使TestNewParent的初始化,NewKid沒有被初始化。

若是一個字段既是static 的又是final的,而且使用一個編譯時常量表達式初始化,使用這樣的字段,也不是對聲明該字段類的主動使用。

看下面的代碼:

interface Angry {
    String greeting = "Grrrr!";//常量表達式
    int angerLevel = Dog.getAngerLevel();//很是量表達式,會打包進<clinit>方法,並且調用DOG的靜態方法,主動使用。
}
class Dog{
    static final String greeting = "woof woof world";
    static{
        System.out.println("Dog was initialized");
    }
    /** * 靜態方法 */
    static int getAngerLevel(){
        System.out.println("Angry was initialized");
        return 1;
    }
}
class Example01{
    public static void main(String[] args) {
        System.out.println(Angry.greeting);
        System.out.println(Dog.greeting);
    }
    static{
        System.out.println("Example was initialized");
    }
}
class Example02{
    public static void main(String[] args) {
        System.out.println(Angry.angerLevel);
    }
    static{
        System.out.println("Example was initialized");
    }
}
複製代碼

Example01 只是引用了靜態常量(常量表達式形式的初始化)Angry.greetingDog.greeting,因此編譯時已經替換爲了實際數值,不屬於主動使用,不會初始化對應的類。
Example01 的輸出:

Example01 was initialized
Grrrr!
woof woof world
複製代碼

Example02引用了Angry.angerLevel,雖然是靜態常量,可是是經過方法調用的方式Dog.getAngerLevel()初始化數值,屬於主動使用Angry。而Angry調用了Dog的靜態方法getAngerLevel(),屬於主動使用Dog
Example02 的輸出:

Example02 was initialized
Dog was initialized
Angry was initialized
1
woof woof world
複製代碼

類型的實例--對象的建立和終結

一旦一個類被裝載鏈接初始化。它就隨時可用了。程序訪問它的靜態字段,調用它的靜態方法,或者建立它的實例。

對象的建立--類實例化

在Java程序中,類能夠被明確或者隱含的實例化。明確的實例化一個類有四種途徑:

  • 明確使用new操做符
  • 調用Classjava.lang.reflect.Constructor對象的newInstance()
  • 調用現有對象的clone()方法
  • 經過java.io.ObjectInputStream類的getObject()方法反序列化

另外存在下面幾種隱含實例化類的方式:

  • 對於Java虛擬機裝載的每個類,它會暗中實例化一個Class對象來表明這個類型。
  • 當Java虛擬機裝載了在常量池中包含CONSTANT_String_info入口的類的時候,它會建立新的String對象的實例來表示這些常量字符串。
  • 經過執行包含字符串鏈接操做符的表達式產生對象。

以下代碼示例:

class Example{
    public static void main(String[] args) {
        if (args.length != 2){
            System.out.println("illegal args");
            return;
        }
        System.out.println(args[0] + args[1]);
    }
    static{
        System.out.println("Example was initialized");
    }
}
複製代碼

字節碼內容:

public static main([Ljava/lang/String;)V L0 LINENUMBER 20 L0 ALOAD 0 ARRAYLENGTH ICONST_2 IF_ICMPEQ L1 L2 LINENUMBER 21 L2 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "illegal args"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 22 L3
    RETURN
   L1
    LINENUMBER 24 L1
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 0
    ICONST_0
    AALOAD
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    ICONST_1
    AALOAD
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 25 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    MAXSTACK = 4
    MAXLOCALS = 1
複製代碼

請關注 args[0] + args[1],編譯器會建立StringBuilder實例,經過 StringBuilder.append 鏈接,再經過 StringBuilder.toString 轉成 String 對象

當 Java 虛擬機建立一個類的新實例時,無論明確的仍是隱含的,首先都須要在堆中爲保存對象的實例變量分配內存,包括在當前類和它的超類中所聲明的變量。一旦虛擬機爲新的對象準備好了堆內存,它當即把實例變量初始化爲默認初始值(虛擬機默認值)。隨後纔會爲實例變量賦予正確的初始值(碼農指望的)。

Java 編譯器爲它編譯的每個類都至少生成一個實例初始化方法(構造函數)。在 Java class文件種,實例初始化方法被稱爲<init>。針對源碼中每個類的構造方法,Java 編譯器都產生一個對應的<init>方法。若是類沒有明確聲明構造方法,編譯器默認產生一個無參構造方法。

構造方法代碼示例:

class ExampleCons{
    private int width = 3;

    public ExampleCons() {
        this(1);
        System.out.println("ExampleCons(),width = " + width);
    }

    public ExampleCons(int width) {
        this.width = width;
        System.out.println("ExampleCons(int),width = " + width);
    }
    public ExampleCons(String msg) {
        super();
        System.out.println("ExampleCons(String),width = " + width);
        System.out.println(msg);
    }

    public static void main(String[] args) {
        String msg = "Test Constructor MSG";
        ExampleCons one = new ExampleCons();
        ExampleCons two = new ExampleCons(2);
        ExampleCons three = new ExampleCons(msg);
    }
}
複製代碼

控制檯輸出:

ExampleCons(int),width = 1
ExampleCons(),width = 1
ExampleCons(int),width = 2
ExampleCons(String),width = 3
Test Constructor MSG
複製代碼

而從字節碼上看,全部的<init>方法都默認執行了父類的無參構造方法:

// class version 52.0 (52)
// access flags 0x20
class hua/lee/jvm/ExampleCons {

  // compiled from: Angry.java

  // access flags 0x2
  private I width

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 39 L0
    ALOAD 0
    ICONST_1
    INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V
   L1
    LINENUMBER 40 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD hua/lee/jvm/ExampleCons.width : I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 41 L2
    RETURN
   L3
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L3 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x1
  public <init>(I)V
   L0
    LINENUMBER 43 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 36 L1
    ALOAD 0
    ICONST_3
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L2
    LINENUMBER 44 L2
    ALOAD 0
    ILOAD 1
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L3
    LINENUMBER 45 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(int),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 46 L4
    RETURN
   L5
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
    LOCALVARIABLE width I L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x1
  public <init>(Ljava/lang/String;)V
   L0
    LINENUMBER 48 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 36 L1
    ALOAD 0
    ICONST_3
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L2
    LINENUMBER 49 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(String),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD hua/lee/jvm/ExampleCons.width : I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 50 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 51 L4
    RETURN
   L5
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
    LOCALVARIABLE msg Ljava/lang/String; L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V L0 LINENUMBER 54 L0 LDC "Test Constructor MSG" ASTORE 1 L1 LINENUMBER 55 L1 NEW hua/lee/jvm/ExampleCons DUP INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> ()V ASTORE 2 L2 LINENUMBER 56 L2 NEW hua/lee/jvm/ExampleCons DUP ICONST_2 INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V ASTORE 3 L3 LINENUMBER 57 L3 NEW hua/lee/jvm/ExampleCons DUP ALOAD 1 INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (Ljava/lang/String;)V ASTORE 4 L4 LINENUMBER 58 L4 RETURN L5 LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE msg Ljava/lang/String; L1 L5 1
    LOCALVARIABLE one Lhua/lee/jvm/ExampleCons; L2 L5 2
    LOCALVARIABLE two Lhua/lee/jvm/ExampleCons; L3 L5 3
    LOCALVARIABLE three Lhua/lee/jvm/ExampleCons; L4 L5 4
    MAXSTACK = 3
    MAXLOCALS = 5
}
複製代碼

對象的終結--垃圾收集

對於Java程序來講,程序能夠明確或隱含的爲對象分配內存,可是不能明確的釋放內存。咱們前面講過,Java 虛擬機的實現應該具備某種自動堆儲存管理策略,而大部分採用垃圾收集器。當一個對象再也不被程序所引用了,虛擬機必須回收那部份內存。

若是類聲明瞭一個fianlize()的方法,垃圾回收器會在釋放這個實例佔據的內存空間前執行這個方法。而垃圾收集器針對一個對象只會調用一次fianlize()。若是執行fianlize()期間對象被從新引用(復活了),隨後又不被引用,垃圾收集器也不會再次執行fianlize()方法。

垃圾收集器篇幅較長,本篇還是以生命週期爲主線,後面詳細記錄

類型生命週期的結束--卸載類型

Java 類的生命週期和對象的生命週期很像。

  • 對象:虛擬機建立並初始化對象,是程序可以使用對象,而後在對象變得再也不被引用時可選地進行垃圾收集。
  • 類:虛擬機裝載、鏈接、初始化類,是程序能使用類,當程序再也不引用這些類時可選地卸載它們。

類的垃圾收集和卸載之因此在虛擬機種很重要,是由於 Java 程序能夠在運行時經過用戶自定義的類裝載器裝載類型來動態地擴展程序。多有被裝載嗯類型都在方法區佔據內存空間。若是Java 程序持續經過用戶自定義的類裝載器裝載類型,方法區的內存足跡就會不斷增加。若是某些動態裝載的類型只是臨時須要,當他們再也不被引用以後,佔據的空間能夠經過卸載類型而釋放。

使用啓動類裝載器裝載的類型永遠是可觸及的,因此永遠不會被卸載。只有使用用戶自定義的類裝載器裝載的類型纔會變成不可觸及的。
判斷動態裝載的類型的 Class 實例是否能夠觸及有兩種方式:

  • 若是程序保持對 Class 實例的明確引用,它就是可觸及的
  • 若是堆中還存在一個可觸及對象,在方法區中它的類型數據指向一個 Class 實例,那麼這個 Class 實例就是可觸及的。

對於可觸及,能夠看下面這個類,Java 虛擬機須要創建起一個完整的引用(觸及)鏈。

class MyThread extends Thread implements Cloneable{
}
複製代碼

引用關係圖形化描述以下:

image

從可觸及的MyThread對象指針開始,垃圾收集器跟隨一個指向MyThread類型數據的指針,它找到了:

  • 一個指向堆中的MyThread的 Class 類的實例引用。
  • 一個指向MyThread的實現的接口 Cloneable 的類型數據指針。
  • 一個指向MyThread的直接超類Thread 的類型數據的指針。

從 Cloneable 的類型數據開始,垃圾收集找到了:

  • 一個指向堆中Cloneable的 Class 實例的引用。

從 Thread 的類型數據開始,垃圾收集器找到了:

  • 一個指向堆中的 Thread 的 Class 類的實例引用。
  • 一個指向 Thread 直接超類的 Object 的類型數據的指針。
  • 一個指向 Thread 的實現接口 Runnable 的類型數據的指針。

從 Runnable 的類型數據中,垃圾收集器找到了:

  • 一個指向堆中的 Runnable 的 Class 類實例的引用。

從 Object 的類型數據中,垃圾收集器找到了:

  • 一個指向堆中的 Object 的 Class 類實例的引用。

能夠看到從一個 MyThread 對象開始,垃圾收集器能夠觸及和 MyThread 有關的全部信息。 PS:設計真滴是優秀啊

結束

Java 類型和對象的生命週期就學習到這裏。在類鏈接垃圾回收部分只是作了簡單介紹,後面會補上。

下面一篇詳細介紹類的鏈接過程-鏈接模型

相關文章
相關標籤/搜索