Java類的鏈接與初始化 (及2013阿里初始化筆試題解析)

 Java虛擬機經過裝載、鏈接、初始化來使得一個Java類型能夠被Java程序所使用,以下圖所示,其中鏈接過程又分爲驗證、準備、解析三個部分。其中部分類的解析過程能夠推遲到程序真正使用其某個符號引用時再去解析。
這裏寫圖片描述
解析過程能夠推遲到類的初始化以後再進行,但這是有條件的,Java虛擬機必須在每一個類或接口主動使用時進行初始化。
如下爲主動使用的狀況:java

  1. 不管直接經過new建立出來的,仍是經過反射、克隆、序列化建立的)建立某個類新的實例
  2. 使用某個類的靜態方法
  3. 訪問某個類或接口的靜態字段
  4. 調用JavaAPI中的某些反射方法
  5. 初始化某個類的子類(要求其祖先類都要被初始化,不然沒法正確訪問其繼承的成員)
  6. 啓動某個標明爲啓動類的類(含有main()方法)

主動使用會致使類的初始化,其超類均將在該類的初始化以前被初始化,但經過子類訪問父類的靜態字段或方法時,對於子類(或子接口、接口的實現類)來講,這種訪問就是被動訪問,或者說訪問了該類(接口)中的不在該類(接口)中聲明的靜態成員。
如:
Grandpa的定義以下:安全

package com.ice.passiveaccess;

public class Grandpa {
    static{
        System.out.println("Grandpa was initialized.");
    }
}

Parent的定義以下:數據結構

package com.ice.passiveaccess;

public class Parent extends Grandpa{
    static String language = "Chinese";
    static{
        System.out.println("Parent was initialized.");
    }
}

Cindy的定義以下:函數

package com.ice.passiveaccess;

public class Cindy extends Parent{
    static{
        System.out.println("Child was initialized.");
    }
}

如今經過Cindy訪問父類的language成員this

package com.ice.passiveaccess;

public class PassiveAccessTest {
    public static void main(String args[]){
        System.out.println(Cindy.language);
    }
}

結果以下:code

這裏寫圖片描述
可見這是被動訪問,Cindy自身並無初始化對象

下面簡要介紹裝載、驗證與初始化過程:
1.裝載:繼承

  1. 找到該類型的class文件,產生一個該類型的class文件二進制數據流(ClassLoader須要實現的loadClassData()方法)
  2. 解析該二進制數據流爲方法區內的數據結構
  3. 建立一個該類型的java.lang.Class實例

在加載器的相關代碼中能夠看到,最終經過defineClass()建立一個Java類型對象(Class對象)。遞歸

2.驗證:
class文件校驗器須要四趟獨立的掃描來完成驗證工做,其中:
第一趟掃描在裝載時進行,會對class文件進行結構檢查,如接口

  1. 對魔數進行檢查,以判斷該文件是不是一個正常的class文件
  2. 對主次版本號進行檢查,以判斷class文件是否與java虛擬機兼容
  3. 對class文件的長度和類型進行檢查,避免class文件部分缺失或被附加內容。

第二趟掃描在鏈接過程當中進行,會對類型數據進行語義檢查,主要檢查各個類的二進制兼容性(主要是查看超類和子類的關係)和類自己是否符合特定的語義條件

  1. final類不能擁有子類
  2. final方法不能被重寫(覆蓋)
  3. 子類和超類之間沒有不兼容的方法聲明
  4. 檢查常量池入口類型是否一致(如CONSTANT_Class常量池的內容是否指向一個CONSTANT_Utf8字符串常量池)
  5. 檢查常量池的全部特殊字符串,以肯定它們是不是其所屬類型的實例,以及是否符合特定的上下文無關語法、格式

第三趟掃描爲字節碼驗證,其驗證內容和實現較爲複雜,主要檢驗字節碼是否能夠被java虛擬機安全地執行。
第四趟掃描在解析過程當中進行,爲對符號引用的驗證。在動態鏈接過程當中,經過保存在常量池的符號引用查找被引用的類、接口、字段、方法時,在把符號引用替換成直接引用時,首先須要確認查找的元素真正存在,而後須要檢查訪問權限、查找的元素是不是靜態類成員而非實例成員。

3.準備:
爲類變量分配內存、設置默認初始值(內存設置初始值,而非對類變量真正地進行初始化,即類中聲明int i = 5,但實際上這裏是分配內存並設置初始值爲0)

4.解析:
在類的常量池中尋找類、接口、字段、方法的符號引用,將這些符號引用替換成直接引用

5.初始化:
對類變量賦予指定的初始值(這個時候int i = 5就必須賦予i以初值5)。這個初始值的給定方式有兩種,一種是經過類變量的初始化語句,一種是靜態初始化語句。而這些初始化語句都將被Java編譯器一塊兒放在方法中。
如前面所述,一個類的初始化須要初始化其直接超類,並遞歸初始化其祖先類,初始化是經過調用類的初始化方法完成的。此外,對於接口,並不須要初始化其父接口,而只須要執行該接口的接口初始化方法就能夠了。
注意:

  • 在初始化階段,只會爲類變量(靜態全局變量)進行初始化工做,而且當類變量聲明爲final類型切初始化語句採用了常量表達式方式進行初始化賦值,那麼, 也不會對其進行初始化,它將會直接被編譯器計算並保存在常量池中,而且對這些變量的使用也將直接將其變量值嵌入到字節碼中。

         如UsefulParameter類以下:

Class UsefulParameter{
   static final int height = 2;
   static final int width = height * 2; 
}

         類Area的類變量初始化以下:

Class Area{ 
   static int height = UsefulParameter.height * 2 ; 
   static int width = UsefulParameter.width * 2;
}

         在Area的< clinit>中,將直接把二、4嵌入到字節碼中

         

  • 接口的初始化與類有所不一樣,在初始化階段,會爲在接口中聲明的全部public、static和final類型的、沒法被編譯爲常量的字段進行初始化

6.類實例化
這裏須要明白什麼是類初始化,什麼是類實例化,以及類的實例對象的初始化
如前面所述,類初始化時對類(靜態)變量賦予指定的初始值,類初始化以後就能夠訪問類的靜態字段和方法,而訪問類的非靜態(實例)字段和方法,就須要建立類的對象實例,故類的實例化是在類的初始化以後,是在堆上建立一個該類的對象。
類的靜態方法和字段屬於類,做爲類型數據保存在方法區,其生命週期取決於類,而實例方法和字段位於Java堆,其生命週期取決於對象的生命週期。
  類的初始化會從祖先類到子類、按出現順序,對類變量的初始化語句、靜態初始化語句塊依次進行初始化。而對類實例的初始化也相似,會從祖先類到子類、按出現順序,對類成員的初始化語句、實例初始化塊、構造方法依次進行初始化。
好比:

package com.ice.init;

public class Parent {
    public static int i = print("parent static:i");
    public int ii = print("parent:ii");
    static{
        print("父類靜態初始化");
    }

    {
        print("父類實例初始化");
    }

    public Parent(String str) {
        System.out.println("parent constructor:" + str);
    }

    public static int print(String str){
        System.out.println("initial:" + str);
        return i;
    }
}

子類Child以下:

package com.ice.init;

public class Child extends Parent{
    public static int i = print("child static:i");
    public int ii = print("child:ii");

    static{
        print("子類靜態初始化");
    }

    {
        print("子類實例初始化");
    }

    public Child(String str) {
        super(str);
        System.out.println("Child constructor:" + str);
    }

    public static int print(String str){
        System.out.println("initial:" + str);
        return i;
    }

    public static void main(String args[]){
        Child child = new Child("cindy");
    }
}

其初始化順序爲:


Java編譯器爲每一個類生成了至少一個實例初始化方法< init >,一個< init >方法分爲三部分: 另外一個初始化方法< init >(),對任意實例成員的初始化的字節碼,構造方法的方法體的字節碼
< init >方法的調用以下:
若< init >指明從this()方法明確調用另外一個構造方法,那麼將調用另外一個構造方法,不然,若該類有直接超類,那麼,若< init >指明從super()方法明確調用其超類的構造方法,那麼將調用超類的構造方法,不然,將默認調用超類的無參構造方法。這樣,將從其祖先類到該 類,分別完成對應的實例成員的初始化(可能被子類覆蓋)


接下來以一道題結束本節:
判斷輸出:

package com.ice.init;

class T  implements Cloneable{
      public static int k = 0;
      public static T t1 = new T("t1");
      public static T t2 = new T("t2");
      public static int i = print("i");
      public static int n = 99;

      public int j = print("j");
      {
          print("構造塊");
      }

      static {
          print("靜態塊");
      }

      public T(String str) {
          System.out.println((++k) + ":" + str + "    i=" + i + "  n=" + n);
          ++n; ++ i;
      }

      public static int print(String str){
          System.out.println((++k) +":" + str + "   i=" + i + "   n=" + n);
          ++n;
          return ++ i;
      }

      public static void main(String[] args){
          T t = new T("init");
      }
    }

題解以下:

(1). 首先T類被加載、鏈接後進行初始化,會先對字段k、t一、t二、i、n以及static塊進行初始化。 
(2). t1實例的初始化會初始化實例成員j,(實際上先進行父類實例內容的初始化)先調用靜態方法print,並執行實例初始化塊{},輸出: 
 1: j i=0 n= 0(i和n都尚未初始化) 
 2:構造塊 i=1 n=1 
(3). 隨後調用t1實例的構造函數,輸出: 
 3:t1 i=2 n=2 
(4). 相似有t2實例的初始化: 
 4: j i=3 n= 3 
 5:構造塊 i=4 n=4 
 6:t2 i=5 n=5 
(5). i的初始化: 
 7.i i=6 n=6 
(6). n的初始化和靜態塊的初始化: 
 8.靜態塊 i=7 n=99(n已經被初始化) 
(7). t實例的初始化:   9.j i=8 n= 100   10.構造塊 i=9 n= 101   11.init i=10 n= 102

相關文章
相關標籤/搜索