(一)類加載、對象初始化

一、類加載

類的生命週期

類從被加載到虛擬機內存中開始,到卸載出內存爲止。它的整個生命週期可以分爲:加載、驗證、準備、解析、初始化、使用、卸載。

類初始化階段

對加載的階段,jvm並沒有強制性規定。但是初始化階段,jvm中嚴格規定了時機,(而加載、驗證、準備自然需要在此之前開始)

有且僅有以下五種的情況纔會出現類的初始化。

1)遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。

最常見的Java代碼場景是:

  • 使用new關鍵字實例化對象時
  • 讀取或設置一個類的靜態字段(static)時(被static修飾又被final修飾的,已在編譯期把結果放入常量池的靜態字段除外)
  • 調用一個類的靜態方法時。

2)使用Java.lang.refect包的方法對類進行反射調用時,如果類還沒有進行過初始化,則需要先觸發其初始化。
3)繼承關係中,當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
4)當虛擬機啓動時,用戶需要指定一個要執行的主類,虛擬機會先執行該主類。啓動類所在類會被初始化(main方法所在類)
5)當使用JDK1.5支持時,如果一個java.langl.incoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始
 

類的加載過程

1、加載階段
類的加載階段是將class文件中的二進制數據讀取到內存中,然後將該字節流所代表的靜態存儲結構轉化爲方法區中運行時的數據結構,並且在堆內存中生成一個該類的java.lang.class對象,作爲方法區數據結構的入口。

2、連接階段
類的連接階段包括三個小的過程:分別是驗證、準備和解析。
(1)驗證
驗證在連接階段中的主要目地是確保class文件的字節流所包含的內容符合JVM規範,並且不會出現危害JVM自身安全的代碼

(2)準備
準備階段主要做的事就是在方法區爲靜態變量分配內存以及賦初始默認值(區別於初始化階段賦的程序指定的真實值)
注意:final修飾的靜態常量在編譯階段就已經賦值,不會導致類的初始化,是一種被動引用,因此也不存在連接階段。

(3)解析
解析就是在常量池中尋找類、接口、字段和方法的符號引用,並且將這些符號引用替換成直接引用的過程
解析過程主要針對類接口、字段、類方法和接口方法四類進行。

3、初始化階段
初始化階段是類的加載過程的最後一個階段,該階段主要做一件事情就是執行< clinit>(),該方法會爲所有的靜態變量賦予正確的值。
關於靜態變量賦值注意以下幾點:

    1)編譯器的收集順序是由語句在源文件中出現的順序決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變量。定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問。靜態語句塊可以對後面的靜態變量賦值,但不能對其進行訪問。

public class Test{
static{
         i=0;//賦值可以,編譯正常通過
        System.out.println(i);//訪問出錯,非法的前向訪問
    }
    static int i=1;

}

2)父類優先執行< clinit>()方法,父類的靜態語句塊要優先於子類的變量賦值操作。

3)< clinit>()方法非必需的,如果一個類沒有靜態語句塊,也沒有對變量的賦值操作。編譯器可以不爲這個類生成< clinit>()方法。

4)虛擬機保證一個類的< clinit>()方法在多線程環境中只有一個線程去執行這個方法,其他線程處於阻塞等待,直到活動線程執行< clinit>()方法完畢。

類加載器

  • 前言

1)概念:類加載階段的第一個動作(類加載的第一個階段是加載階段包含三個動作):通過類的全限定名來獲取描述此類的二進制字節流,這個動作放到java虛擬機外部去實現,讓應用程序決定去如何獲取需要的類,實現這個動作的代碼稱爲類加載器。

2)作用:源碼以及編譯好的字節碼文件,存儲在硬盤上。需要通過類加載器將文件放在運行時內存的方法區中。(加載到java虛擬機中)

3)影響:對於兩個類來源同一個class文件,被同一個虛擬機加載,只要加載他們的類加載器不同,這兩個類也不會相同。所以在Java中任意一個類都是由這個類本身和加載這個類的類加載器來確定這個類在JVM中的唯一性。

  • 類加載器分類

從頂的父類加載器到底層的子類加載器分別是:啓動類加載器、擴展類加載器、應用程序類加載器、自定義類加載器。

  • 啓動類加載器:C++語言實現,是虛擬機自身的一部分
  • 其餘的類加載器:由java實現,獨立於虛擬機外部,繼承抽象類java.lang.ClassLoader

擴展類加載器:開發者可以直接使用擴展類加載器。

應用程序類加載器:負責加載用戶類路徑上所指定的類庫,開發者可以直接使用,如果應用程序沒有自定義類加載器,這就是默認的加載器。

  • 好處

Java類隨着它的類加載器具備了優先級的層次關係。如果自定義了一個OBJECT類,系統將會出現多個object類,最基礎行爲無法保證。

  • 雙親加載機制

1)工作原理

       如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,直到傳送到頂層父類加載器去完成。當父類加載器的搜索範圍沒有找到所需要類時,子類纔會嘗試自己去加載。

2)實現的原理

這裏的父子關係使用組合關係複用父類加載器的代碼。

繼承關係和組合關係?

      繼承關係:虧使用原有類的功能,並且無需要重複編寫原有類的情況下,對原有類的功能進行擴展。一個類(稱爲子類、子接口)繼承另外的一個類(稱爲父類、父接口)的功能,並可以增加它自己的新功能的能力。

      組合關係:實現:在一個類中創建原有類的對象,重複利用原有類的功能。兩個類緊密耦合在一起 它們有相同的生命週期

3)實現

       實現代碼在java.lang.ClassLoader()中的loadClass方法中。

步驟一:先檢查是否被加載過

步驟二:如果沒有被加載過,則調用父類加載器的loadClass方法,

步驟三:若父類加載器爲空,默認使用啓動類加載器作爲父加載器。

步驟四:如果父類加載失敗,拋出異常後,再調用自身的findClass方法進行加載。

  • 破壞雙親委派模型

  1. JDK1.2之後雙親委派模型纔出現,之前是不符合雙親委派模型的
  2. 基礎類需要回調用戶的代碼, 線程上下文類加載器,默認的是應用程序類加載器,父類加載器可以請求子類去完成類的加載動作。比如:JNDI、JDBC中
  3. 代碼熱替換,由於用戶對程序動態性的追求而導致,代碼熱替換,在不重啓程序的時候,對某些程序模塊進行替換。這就需要每一個程序模塊都有一個自己的類加載器,當需要替換這個程序模塊時,就要把該程序塊連同類加載器一起替換掉以實現代碼的熱替。

二、對象初始化

1.對象的內存佈局

在 Hotspot 虛擬機中,對象在內存中的佈局可以分爲 3 塊區域:對象頭實例數據對齊填充

1)對象頭包括兩部分信息,第一部分:markword用於存儲對象自身的運行時數據(對象哈希碼、GC 分代年齡、是否是偏向鎖、鎖狀態標誌等等),另一部分是類型指針,用來確定這個對象屬於哪個類的實例。

2)實例數據:是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。

3)對齊填充:保證對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

2.對象創建的過程

  1. 類加載檢查
  2. 分配內存(java堆中)
  3. 初始化零值
  4. 設置對象頭
  5. 執行init方法
  • 類加載檢查 虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
  • 分配內存 在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後便可確定,爲對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中劃分出來。分配方式“指針碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定
  • 初始化零值:內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值,非static修飾的成員變量(不包括對象頭),保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用。
  • 設置對象頭:對象的對象頭包含兩類信息:

        1)用於存儲對象自身的自身運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),

        2)類型指針,用來確定這個對象屬於哪個類的實例。

  • 執行 init 方法:從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛開始,<init> 方法還沒有執行,所有的字段都還爲零。所以一般來說,執行 new 指令之後會接着執行構造方法。

3.對象的訪問定位

       建立對象就是爲了使用對象,我們的 Java 程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:

  1. 句柄: 如果使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據(java堆中)與類型數據(方法區中)各自的具體地址信息;
  2. 直接指針: 如果使用直接指針訪問, reference 中存儲的直接就是對象實例數據的地址

 

 

 

 

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