java虛擬機學習筆記——類型和對象的生命週期(第七章)

概述

這一章以一個Java類型(類或接口)的生命週期爲例來討論開始階段的裝載、連接和初始化,以及佔Java類型生命週期絕大部分時間的對象實例化、垃圾收集和對象終結,然後是java類型生命週期的結束,也就是從虛擬機中卸載。

7.1、類型裝載、連接與初始化

Java虛擬機通過裝載、連接和初始化一個Java類型,使該類型可以被正在運行的java程序所使用。其中,類裝載就是把二進制形式的Java類型讀入Java虛擬機中;而連接就把這種已經讀入虛擬機的二進制形式的類型數據合併到虛擬機的運行時狀態中去。連接階段分爲三個子步驟——驗證、準備和解析。“驗證”步驟確保了Java類型數據格式正確並且適於Java虛擬機使用。“準備”步驟則負責爲該類型分配它所需的內存,比如爲它的類變量分配內存。“解析”步驟則負責把常量池中的符號引用轉換爲直接引用。虛擬機的實現可以推遲解析這一步,它可以在當運行中的程序真正使用某個符號引用時再去解析它。之後,進行初始化,在初始化期間,都將給類變量賦以適當的初始值。


在類和接口被裝載和連接的時機上,java虛擬機規範沒有嚴格的規定,但是它嚴格的定義了初始化的時機,所有的Java虛擬機實現必須在每個類或接口首次主動使用時初始化。下面六種情況符合主動使用的要求:

1)、當創建某個類的實例時(通過new指令或通過不明確的創建、反射、克隆、反序列化)

2)、當調用某個類的靜態方法時

3)、當使用某個類或接口的靜態字段,或者對該字段賦值時。(final修飾的靜態字段除外,它被初始化爲一個編譯時的常量表達式)

4)、當調用Java API中的某些反射方法時,比如類Class中的方法或者java.lang.reflect包中的類的方法

5)、當初始化某個類的子類時(某個類初始化時,要求它的超類已經被初始化了)

6)、當虛擬機啓動時某個被標明爲啓動類(即含有main()方法的那個類)

注:任何一個類的初始化都要求它的所有祖先類預先初始化,而一個接口的初始化,並不要求它的祖先接口預先被初始化。也就是說只有某個接口所聲明的非常量字段被使用時,該接口才會被初始化,而不會因爲實現這個接口的子接口或類要初始化而被初始化。但是當實現了父接口的子類(或擴展了父接口的子接口)被裝載時,父接口也必須被裝載。

7.1.1裝載

裝載階段有三個基本動作組成,要裝載一個類型,Java虛擬機必須:

1)、通過該類型的完全限定名,產生一個代表該類型的二進制數據流。

2)、解析這個二進制數據流爲方法區內的內部數據結構。

3)、創建一個表示該類型的java.lang.Class類型的實例 (裝載步驟最終的產品就是這個Class類的實例對象,它成爲程序與內部數據結構之間的接口,要訪問關於該類型的信息,程序就要調用該類型對應的Class實例對象的方法)。

7.1.2驗證

裝載後就要進行連接了,連接過程的第一步是驗證——確認類型符合Java語言的語義,並

且它不會危及虛擬要的完整性。

其實有一些特定的檢查並不發生在驗證這一步,如:1)、在裝載過程中,虛擬機大多會檢查

二進制數據以確保數據全部都是預期的格式(符合java class文件格式);2)、在裝載時,還

要確保除了Object類之外的每一個類都有一個超類,因爲裝載一個類的時候必須確保該類

的所有超類都已經被裝載了。3)、還有一種檢查往往發生在驗證階段之後,那就是符號引用

的驗證,當虛擬機搜尋一個被符號引用的元素(類型、字段和方法)時,它必須首先確認該

元素存在,如果虛擬機發現元素存在,它必須進一步檢查引用類型有訪問該元素的權限。

那麼驗證階段做哪些檢查呢,如下:

1、 確保各個類之間二進制兼容的檢查

1)、檢查final的類不能擁有子類。

2)、檢查final的方法不能被覆蓋

3)、確保在類型和超類型之間沒有不兼容的方法聲明(例如兩個方法擁有同樣的名字,參數在數量和順序類型上都相同,但是返回類型不同)

2、檢查所有的常量池入口相互之間一致

3、檢查常量池中的所有的特殊字符串(類名、字段名、和方法名、字段描述符和方法描述符是否符合格式)

4、檢查字符字節碼的完整性(這是最複雜的任務,所有java虛擬機都必須設法爲它們執行的每個方法檢驗字節碼的完整性)

7.1.3準備

在準備階段,Java虛擬機爲類變量分配內存,設置默認初始值。但到達初始化階段之前,類變量都沒有被初始化爲真正的初始值,在準備階段是不會執行java代碼的。

Char類型默認爲’\u0000’,byte默認爲(byte)0,boolean默認爲0,float默認爲0.0f,double默認爲0.0d,long默認爲0L。

7.1.4 解析

解析過程就是在類型的常量池中尋找類、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過程。

7.1.5 初始化

在java代碼中,一個正確的初始值是通過類變量初始化語句或者靜態初始化語句給出的。

所有的類變量初始化語句和類型的靜態初始化器都被java編譯器收集在一起,放到一個特殊的方法中,稱爲類初始化方法。在類和接口的Java class文件中,這個方法被稱爲”<clinit>”。通常的Java程序方法是無法調用這個<clinit>方法的,只能被java虛擬機調用。

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

1)、如果類存在直接超類的話,且直接超類還沒有被初始化,就先初始化直接超類。

2)、如果類存在一個類初始化方法(<clinit>)就執行此方法。

注:初始化一個接口只需一步:如果接口存在一個接口初始化方法的話,就執行此方法。

初始化的順序按照類變量初始化語句和靜態初始化語句出現的順序初始化。

並不是所有的類都需要在它的class文件中有一個<clinit>()方法。如果類沒有聲明任何類變量,也沒有靜態初始化語句,那麼它就不會有<clinit>()方法。如果類聲明瞭類變量,但是沒有明確使用類變量初始化語句或者靜態初始化語句初始化它們,那麼類也不會有<clinit>()方法。

所有在接口中聲明的隱式公開(publict)、靜態(static)、最終(final)字段必須在字段初始化語句中初始化,如果接口包含任何不能在編譯時被解析成爲一個常量的字段初始化語句,接口就擁有一個<clinit>()方法。例如:

interface Example{

       intketchup = 5;

    int mustard= (int) (Math.random()*5.0);

}



ketchup將會被初始化爲一個編譯時常量,而mustard字段被<clinit>()方法初始化。

主動使用與被動使用

在第六章中曾提到主動使用類類型的6種情況,他們會引發初始化。當使用一個非常量的靜態字段時,只有這個字段是被當前類或接口聲明的情況下才是主動使用,如果是子類使用父類中聲明的字段,子接口和實現了該接口的類使用此接口中的字段都被認爲是被動使用,不會引發初始發,例如:

public class NewParent {

    static int hoursOfsleep = (int)(Math.random()*3.0);

    static{

       System.out.println("NewParentwas initialized");

    }

}

public class NewbornBaby extends NewParent {

 

    static int housOfCrying = 6+(int)(Math.random()*2.0);

    static{

       System.out.println("NewbornBabywas initialized.");

    }

}

public class Example {

    static{

       System.out.println("Example wasinitialized.");

    }

    public static void main(String[] args) {

       int hours = NewbornBaby.hoursOfsleep;

       System.out.println(hours);

    }

}


運行結果:

Example was initialized.

NewParent was initialized

2

7.2 對象的生命週期

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

7.2.1類實例化

實例化一個類有四種途徑:明確地使用new操作符;調用Class或者java.lang.reflect.Constructor對象的newInstance()方法;調用任何現有對象的clone()方法;或者通過java.io.ObjectInputStream類的getObject()方法反序列化。除了明確地實例化對象之外,還有幾種情況下會隱含地實例化,例如:1)在任何java程序中第一個隱含實例化對象可能就是保存命令行參數的String對象。2)對於java虛擬機裝載的每一個類型,它會暗中實例化一個Class對象來代表這個類型。3)當Java虛擬機裝載了在常量池中包含CONSTANT_String_info入口的類的時候,它會創建新的String對象的實例來表示這些常量字符串。4)另一種隱含創建對象的途徑是通過執行包含字符串連接操作符的表達式產生對象。

當Java虛擬機創建一個類的實例時,首先都需要在堆中爲保存對象的實例變量分配內存(所有在對象的類中和它的超類中聲明的變量都要分配內存)。準備好內存後,它立即把實例變量初始化爲默認的初始值。一旦虛擬機完成了新對象的分配內存和爲實例變量賦默認初始值後,它隨後就會爲實例變量賦正確的初始值,這一步會有三種情況:1)如果對象是通過clone()調用來創建的,虛擬機把原來被克隆的實例變量中的值拷貝到新對象中。2)如果對象是調用一個ObjectInputStream的readObject()調用反序列化的,虛擬機通過從輸入流中讀入的值來初始化那些非暫時性的實例變量。3)虛擬機調用對象的實例初始化方法。

Java編譯器爲它編譯的每一個類都至少生成一個實例初始化方法。在Java的class文件中,這個實例初始化方法稱爲”<init>”。一個<init>()方法中可能包含三種代碼:調用另一個<init>()方法;實現對任何實例變量的初始化;構造方法體的代碼。

一個類的構造方法有如下幾種情況:

1、 如果構造方法通過明確地調用同一個類中的另一個構造方法(一個this()調用)開始,

它對應的<init>()方法由兩部分組成:

1)、一個同類的<init>()方法的調用

2)、實現了對應構造方法的方法體的字節碼

2、 如果構造方法不是通過一個this()調用開始的,而且這個對象不是Object,<init>()方法則由三部分組成:

1)、一個超類的<init>()方法的調用

2)、任意實例變量初始化方法的字節碼

3)、實現了對應構造方法的方法體的字節碼

3、如果構造方法沒有使用一個this()調用開始,而且這個對象是Object,則上面列表中的第一個元素就不存在,因爲Object沒有超類。

如果構造方法通過明確的調用超類的構造方法(一個super()調用)開始,它的<init>()方法會調用對應的超類的<init>()方法。如果構造方法沒有明確地從this()或者super()調用開始,對應的<init>()方法默認會調用超類的無參數<init>()方法。

7.22 垃圾收集與對象終結

當一個對象不再爲程序所引用了,虛擬機必須回收那部分內存。

7.3 卸載類型

Java虛擬機中類的生命週期和對象的生命週期很相似,虛擬機裝載、連接並初始化類,使程序能使用類,當程序不在引用它們的時候可卸載它們。

類的垃圾收集和卸載之所以在Java虛擬機中很重要,是因爲Java程序可以在運行時通過用戶自定義的類裝載器裝載類型來動態擴展程序。如果程序持續通過用戶自定義的類裝載器裝載類型,方法區的內存就會不斷增長,如果不進行垃圾收集,內存可能被佔滿。

使用啓動類裝載器裝載的類型永遠是可觸及的,所以永遠不會被卸載。只有使用用戶定義的類裝載器裝載的類型纔會變成不可觸及的,從而被虛擬機回收。

判斷動態裝載的類型的Class實例在正常的垃圾收集過程中是否可以觸及有兩種方式:1)如果程序保持對Class實例的明確引用,它就是可觸及的。2)如果在堆中還存在一個可觸及的對象,在方法區中它的類型數據指向一個Class實例,那麼這個Class實例就是可觸及的。


通過類MyThread的實例,垃圾收集器可以“觸及”MyThread和它的所有超類型(Cloneable、Thread、Runnable、Object)的class實例。


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