Java類的加載、連接和初始化

寫在前面:在深度分析Java的ClassLoader機制(源碼級別)中,咱們學習了Java的CLassLoader機制,那麼,JVM將Java類加載完以後,也就是將二進制代碼轉換成java.lang.Class對象以後又作了哪些操做?java

 

1、Java的類加載機制回顧與總結:

咱們知道一個Java類要想運行,必須由jvm將其裝載到內存中才能運行,裝載的目的就是把Java字節代碼轉換成JVM中的java.lang.Class類的對象。這樣Java就能夠對該對象進行一系列操做,裝載過程有兩個比較重要的特徵:層次組織結構和代理模式層次組織結構指的是每一個類加載器都有一個父類加載器,經過getParent()方法能夠獲取到。類加載器經過這種父親-後代的方式組織在一塊兒,造成樹狀層次結構。代理模式則 指的是一個類加載器既能夠本身完成Java類的定義工做,也能夠代理給其它的類加載器來完成。因爲代理模式的存在,啓動一個類的加載過程的類加載器和最終 定義這個類的類加載器可能並非一個。ClassLoader的加載類過程主要使用loadClass方法,該方法中封裝了中加載機制:雙親委派模式安全

通常來講,父類優先的策略就足夠好了。在某些狀況下,可能須要採起相反的策略,即先嚐試本身加載,找不到的時候再代理給父類加載器。這種作法在Java的Web容器中比較常見,也是Servlet規範推薦的作法。好比,Apache Tomcat爲每一個Web應用都提供一個獨立的類加載器,使用的就是本身優先加載的策略。IBM WebSphere Application Server則容許Web應用選擇類加載器使用的策略。
類加載器的一個重要用途是在JVM中爲相同名稱的Java類建立隔離空間。在JVM中,判斷兩個類是否相同,不只是根據該類的二進制名稱,還須要根據兩個類的定義類加載器。只有二者徹底同樣,才認爲兩個類的是相同的。所以,即使是一樣的Java字節代碼,被兩個不一樣的類加載器定義以後,所獲得的Java類也是不一樣的。若是試圖在兩個類的對象之間進行賦值操做,會拋出java.lang.ClassCastException。這個特性爲一樣名稱的Java類在JVM中共存創造了條件。在實際的應用中,可能會要求同一名稱的Java類的不一樣版本在JVM中能夠同時存在。經過類加載器就能夠知足這種需求。這種技術在OSGi中獲得了普遍的應用數據結構

Java類的加載過程:架構

1.經過類的全名產生對應類的二進制數據流。(若是沒找到對應類文件,只有在類實際使用時才拋出錯誤。)
2.分析並將這些二進制數據流轉換爲方法區(JVM 的架構:方法區、堆,棧,本地方法棧,pc 寄存器)特定的數據結構(這些數據結構是實現有關的,不一樣 JVM 有不一樣實現)。這裏處理了部分檢驗,好比類文件的魔數的驗證,檢查文件是否過長或者太短,肯定是否有父類(除了 Obecjt 類)。
3.建立對應類的 java.lang.Class 實例(注意,有了對應的 Class 實例,並不意味着這個類已經完成了加載鏈連接!)。jvm

Java類的連接

Java類的連接指的是將Java類的二進制代碼合併到JVM的運行狀態之中的過程。在連接以前,這個類必須被成功加載。
連接的過程比加載過程要複雜不少,這是實現java的動態性的重要一步!分爲三部分:verification檢測), preparation準備) 和 resolution解析函數

1.verification檢測):
驗證是用來確保Java類的二進制表示在結構上是徹底正確的。若是驗證過程出現錯誤的話,會拋出java.lang.VerifyError錯誤。佈局

linking的resolve會把類中成員方法、成員變量、類和接口的符號引用替換爲直接引用,而在這以前,須要檢測被引用的類型正確性和接入屬 性是否正確(就是public ,private的的問題)諸如,檢查final class 沒有被繼承,檢查靜態變量的正確性等等。
驗證是用來確保Java類的二進制表示在結構上是徹底正確的。若是驗證過程出現錯誤的話,會拋出java.lang.VerifyError錯誤。學習

2.preparation(準備):spa

準備過程則是建立Java類中的靜態域,並將這些域的值設爲默認值。準備過程並不會執行代碼。在一個Java類中會包含對其它類或接口的形式引用,包括它的父類、所實現的接口、方法的形式參數和返回值的Java類等。線程

對類的成員變量分配空間。雖然有初始值,但這個時候不會對他們進行初始化(由於這裏不會執行任何 Java 代碼)。具體以下:
全部原始類型的值都爲 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底層實現大多使用 int),引用類型則爲 null。值得注意的是,JVM 可能會在這個時期給一些有助於程序運行效率提升的數據結構分配空間。

3.resolution(解析):

解析的過程就是確保這些被引用的類能被正確的找到。解析的過程可能會致使其它的Java類被加載。

爲類、接口、方法、成員變量的符號引用定位直接引用(若是符號引用先到常量池中尋找符號,再找先應的類型,無疑會耗費更多時間),完成內存結構的佈局。
這一步是可選的。能夠在符號引用第一次被使用時完成,即所謂的延遲解析(late resolution)。但對用戶而言,這一步永遠是延遲解析的,即便運行時會執行 early resolution,但程序不會顯示的在第一次判斷出錯誤時拋出錯誤,而會在對應的類第一次主動使用的時候拋出錯誤!
另外,這一步與以後的類初始化是不衝突的,並不是必定要全部的解析結束之後才執行類的初始化。不一樣的 JVM 實現不一樣。
看下面一段代碼:

public class LinkTest { public static void main(String[] args) { ToBeLinked toBeLinked = null; System.out.println("Test link."); } }

LinkTest引用了類ToBeLinked,可是並無真正使用它,只是聲明瞭一個變量,並無建立該類的實例或是訪問其中的靜態域。若是把編譯好的ToBeLinkedJava字節代碼刪除以後,再運行LinkTest,程序不會拋出錯誤。這是由於ToBeLinked類沒有被真正用到。連接策略使得ToBeLinked類不會被加載,所以也不會發現ToBeLinked的Java字節代碼其實是不存在的。若是把代碼改爲ToBeLinked toBeLinked = new ToBeLinked();以後,再按照相同的方法運行,就會拋出異常了。由於這個時候ToBeLinked這個類被真正使用到了,會須要加載這個類。

3、Java類的初始化

開發 Java 時,接觸最多的是對象的初始化。實際上類也是有初始化的。相比對象初始化,類的初始化機制要簡單很多。
類的初始化也是延遲的,直到類第一次被主動使用(active use),JVM 纔會初始化類。
當一個Java類第一次被真正使用到的時候,JVM會進行該類的初始化操做。初始化過程的主要操做是執行靜態代碼塊和初始化靜態域。在一個類被初始化之 前,它的直接父類也須要被初始化。可是,一個接口的初始化,不會引發其父接口的初始化。在初始化的時候,會按照源代碼中從上到下的順序依次執行靜態代碼塊 和初始化靜態域。

public class StaticTest { public static int X = 10; public static void main(String[] args) { System.out.println(Y); //輸出60  } static { X = 30; } public static int Y = X * 2; }

在上面的代碼中,在初始化的時候,靜態域的初始化和靜態代碼塊的執行會從上到下依次執行。所以變量X的值首先初始化成10,後來又被賦值成30;而變量Y的值則被初始化成60。
類的初始化分兩步:

1.若是基類沒有被初始化,初始化基類。
2.有類構造函數,則執行類構造函數。

類構造函數是由 Java 編譯器完成的。它把類成員變量的初始化和 static 區間的代碼提取出,放到一個<clinit>方法中。這個方法不能被通常的方法訪問(注意,static final 成員變量不會在此執行初始化,它通常被編譯器生成 constant 值)。同時,<clinit>中是不會顯示的調用基類的<clinit>的,由於 1 中已經執行了基類的初始化。該初始化過程是由 Jvm 保證線程安全的。。

Java類和接口的初始化只有在特定的時機纔會發生,這些時機包括:

建立一個Java類的實例。如 MyClass obj = new MyClass() 調用一個Java類中的靜態方法。如 MyClass.sayHello() Java類或接口中聲明的靜態域賦值。如 MyClass.value = 10 訪問Java類或接口中聲明的靜態域,而且該域不是常值變量。如 int value = MyClass.value 在頂層Java類中執行assert語句。

經過Java反射API也可能形成類和接口的初始化。須要注意的是,當訪問一個Java類或接口中的靜態域的時候,只有真正聲明這個域的類或接口才會被初始化。考慮下面的代碼:

class B { static int value = 100; static { System.out.println("Class B is initialized."); //輸出  } } class A extends B { static { System.out.println("Class A is initialized."); //不會輸出  } } public class InitTest { public static void main(String[] args) { System.out.println(A.value); //輸出100  } }

在上述代碼中,類InitTest經過A.value引用了類B中聲明的靜態域value。因爲value是在類B中聲明的,只有類B會被初始化,而類A則不會被初始化。

 

連接:http://www.hollischuang.com/archives/201

相關文章
相關標籤/搜索