天天看點

2020-09-07Class 對象Java的類加載過程

Class 對象

要了解RRTI在Java中的工作原理,首先必須知道類型資訊在運作時是如何表示的。

這項工作是由被稱為Class對象的特殊對象來完成的,它包含了與類有關的資訊。

事實上,Class對象就是用來建立的所有"正常"對象的。

java使用Class對象來執行其RTTI,即使你正在執行的是類似轉型的這樣的操作。

RTTI

一、RTTI(Run-Time Type identification),通過運作時類型資訊,程式能夠使用基類的指針或引用來檢查這些指針或引用所指向的對象的實際派生類型。

這就是所說的多态,父類對象可以指向子類的引用。

面向對象的程式設計語言C++,Java,delphi都提供了RTTI的支援。RTTI并不是什麼新技術,很早就有了,他主要提供了運作時确定類對象類型的方法。

1.經典造型,如“Shape”,它用RTTI確定造型的正确性,并在遇到一個失敗的造型後産生一個ClassCastException違例。

2.代表對象類型的Class對象。可查詢Class對象,擷取有用的運作期資料。

3. instanceof 告訴我們對象是不是一個特定類型的執行個體。它會傳回一個布爾值,以便以問題的形式使用,就象下面這樣:

if(x instanceof Dog)
((Dog)x).bark();
           
  • 将x造型至一個Dog前,上面的if語句會檢查對象x是否從屬于Dog類。進行造型前,如果沒有其他資訊可以告訴自己對象的類型,那麼instanceof的使用是非常重要的——否則會得到一個ClassCastException違例。

對RTTI來說,編譯器會在編譯期打開和檢查.class檔案。但對“反射”來說,.class檔案在編譯期間是不可使用的,而是由運作時環境打開和檢查 ,我們利用反射機制一般是使用java.lang.reflect包提供給我們的類和方法。

Class 對象

1 類是程式的一部分,每個類都有一個Class對象。換句話說就是,每當編寫了一個新類,就會産生一個Class對象(更準确的說,是被儲存到一個同名的.class檔案中)。為了生成這個類的對象,運作這個程式的Java虛拟機(JVM)将使用被稱為“類加載器”的子系統。【想生成一個對象,你得先将  .class 檔案加載到JVM中去,具體來說是這個JVM的子系統---類加載器】

2-類加載器子系統實際上是可以包含一條類加載器鍊,但是隻有一個原生類加載器【換一句話便是還有其他的擴充的類加載器】,類加載器是JVM【有哪些部分構成??】實作的一部分。原生類加載器加載的是所謂的可信類,包括Java API 類,他們通常是從本地盤加載的。在這條鍊中,通常不需要添加額外的類加載器,但是如果有特殊的需求【以某種特殊的方式加載類,以支援web伺服器應用,或者在網絡中下載下傳類】,你用一種方式可以挂接額外的類加載器。

3-所有的類都是在對其第一次使用時,動态加載到JVM中的。當程式建立第一個對類的靜态成員的引用時,就會加載這個類。這個證明構造方法也是類的靜态方法,即使在構造器的前面沒有添加static關鍵字。是以,使用new操作符建立類的新對象也會被當作對類的靜态成員的引用。

是以,java程式在它開始運作之前并非被完全加載,其各個部分是在必須使用時才加載的。這一點兒與許多傳統語言都不同。動态加載的行為,在c++這樣的靜态語言中很難或者根本無法複制。

4-類加載器首先檢查這個類的Class 對象是否已經加載。如果尚未加載,預設的類加載器就會根據類名查找相關的 .class檔案(例如某個類加載器可能會在資料庫中查找位元組碼)。這個類的位元組碼被加載時,他會接受驗證,以確定其沒有被破壞,并且不包含不良的java代碼。【這是java中用于安全防範目的的措施之一】

一旦某個類的class對象被加載到記憶體中,他就被用來建立這個的所有對象。

一旦定義了類,就可以在類(在java中你所做的全部工作就是定義類,長生哪些類的對象)中設定兩種類型的元素:字段【資料成員】和方法【成員函數】。字段可以是任何類型的對象,可以通過其引用與其進行通信;

普通字段不能在對象間共享。

基本成員的預設值,若類的某個資料成員是基本資料類型,即使沒有初始化,Java也會確定它獲得一個預設值。

當變量作為類的資料成員時,java才確定給其預設值,以確定哪些是基本類型的成員變量得到初始化,【C或C++中沒有這個功能】防止産生程式錯誤。但是這種方法并不适用于“局部”變量(并非某個類的字段,可以是方法中變量)。是以如果是在某個方法中定義int x;那麼便不會被自動初始化為0.是以在使用x前,應先對其賦一個适當的值。如果忘記了指派,那麼java在編譯時會傳回一個錯誤,告訴你變量沒有初始化。這正是Java 優于C++的地方。

在第一次使用方法中沒有被進行初始化的變量時産生的錯誤。

Variable 'x' might not have been initialized

在類的聲明中,屬性是用變量來表示的。這種變量就稱為執行個體變量【資料成員】【執行個體變量在對象建立的時候建立,在對象銷毀的時候銷毀】,是在類聲明的内部但是在類的其他成員方法之外聲明的。類的每個對象維護它自己的一份執行個體變量的副本。

方法的基本組成,名稱、參數、傳回值、方法體。

Java中任何傳遞對象都是傳遞的這個對象的引用。

傳回類型是void 的方法,關鍵字 return 的作用隻是用來推出方法。是以沒有必要到方法結束時才離開,可以在任何地方傳回。但是如果傳回類型不是void,那麼無論在何處傳回,編譯器都會強制傳回一個正确類型的傳回值。

Java的類加載過程

===============Java類的加載、連結、初始化===============

1. 加載:查找并加載類的位元組碼檔案

2. 連結

a.驗證:確定被加載類的正确性,不會破壞JVM的惡意代碼。

b.準備:為類的靜态變量配置設定類存,并執行隐式初始化(有虛拟機把靜态變量初始化為其預設值)

c.解析:把類的符号引用轉換為直接引用

3. 初始化:為類的變量賦予正确的初始值。(執行靜态變量的指派語句、靜态代碼塊)

分析觀察如下代碼:

package com.wzm;

public class Test1 {

    public static void main(String[] args) {

        System.out.println(Singleton.count1);
        System.out.println(Singleton.count2);
    }
}
class Singleton{
    private static Singleton singleton=new Singleton();
    public static int count1;
    public static int count2=0;
    private Singleton() {
        count1++;
        count2++;
    }
    public static Singleton getInstance(){
        return singleton;
    }
}
           

上面一段代碼控制台輸出的結果是:1、0

如果我們了解Java的類加載過程,我們就會了解為什麼上一段代碼的輸出結果是:1、0

下面就開始介紹Java的類加載過程:

上面就是Java類的加載過程,知道了類的加載過程,我們就能分析為什麼上面一段代碼的輸出結果是:1、0

分析:

準備階段:singleton=null;count1=0;count2=0;

初始化階段:執行singleton=new Singleton();這時count1=1,count2=1;

執行count2=0;這時count2又變為0;是以最終的輸出結果為:1、0

其實就是一個類的加載過程中,主要是:類的加載,連結,初始化三步。其中關于連結又分為三步,驗證、準備、解析三個階段。

準備階段:為類的靜态變量配置設定類存,并執行隐式初始化(有虛拟機把靜态變量初始化為其預設值),以及靜态代碼塊配置設定空間。即使是靜态方法沒有調用也不會自動的運作。

初始化階段,則按照程式的順序自動進行初始化。

#類加載的機制的層次結構

每個編寫的".java"拓展名類檔案都存儲着需要執行的程式邏輯,這些".java"檔案經過Java編譯器編譯成拓展名為".class"的檔案,".class"檔案中儲存着Java代碼經轉換後的虛拟機指令,當需要使用某個類時,虛拟機将會加載它的".class"檔案,并建立對應的class對象,将class檔案加載到虛拟機的記憶體,這個過程稱為類加載,這裡我們需要了解一下類加載的過程,如下: 

2020-09-07Class 對象Java的類加載過程
  • 加載:類加載過程的一個階段:通過一個類的完全限定查找此類位元組碼檔案,并利用位元組碼檔案建立一個Class對象
  • 驗證:目的在于確定Class檔案的位元組流中包含資訊符合目前虛拟機要求,不會危害虛拟機自身安全。主要包括四種驗證,檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。
  • 準備:為類變量(即static修飾的字段變量)配置設定記憶體并且設定該類變量的初始值即0(如static int i=5;這裡隻将i初始化為0,至于5的值将在初始化時指派),這裡不包含用final修飾的static,因為final在編譯的時候就會配置設定了,注意這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量是會随着對象一起配置設定到Java堆中。
  • 解析:主要将常量池中的符号引用替換為直接引用的過程。符号引用就是一組符号來描述目标,可以是任何字面量,而直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析(這裡涉及到位元組碼變量的引用,如需更詳細了解,可參考《深入Java虛拟機》)。
  • 初始化:類加載最後階段,若該類具有超類,則對其進行初始化,執行靜态初始化器和靜态初始化成員變量(如前面隻初始化了預設值的static變量将會在這個階段指派,成員變量也将被初始化)。

這便是類加載的5個過程,而類加載器的任務是根據一個類的全限定名來讀取此類的二進制位元組流到JVM中,然後轉換為一個與目标類對應的java.lang.Class對象執行個體,在虛拟機提供了3種類加載器,引導(Bootstrap)類加載器、擴充(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),下面分别介紹

##啟動(Bootstrap)類加載器

啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用C++語言實作的,是虛拟機自身的一部分,它負責将

<JAVA_HOME>/lib

路徑下的核心類庫或

-Xbootclasspath

參數指定的路徑下的jar包加載到記憶體中,注意必由于虛拟機是按照檔案名識别加載jar包的,如rt.jar,如果檔案名不被虛拟機識别,即使把jar包丢到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器隻加載包名為java、javax、sun等開頭的類)。

##擴充(Extension)類加載器

擴充類加載器是指Sun公司(已被Oracle收購)實作的

sun.misc.Launcher$ExtClassLoader

類,由Java語言實作的,是Launcher的靜态内部類,它負責加載

<JAVA_HOME>/lib/ext

目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用标準擴充類加載器。

//ExtClassLoader類中擷取路徑的代碼
private static File[] getExtDirs() {
     //加載<JAVA_HOME>/lib/ext目錄中的類庫
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

           

##系統(System)類加載器

也稱應用程式加載器是指 Sun公司實作的

sun.misc.Launcher$AppClassLoader

。它負責加載系統類路徑

java -classpath

-D java.class.path

指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類加載器,一般情況下該類加載是程式中預設的類加載器,通過

ClassLoader#getSystemClassLoader()

方法可以擷取到該類加載器。

  在Java的日常應用程式開發中,類的加載幾乎是由上述3種類加載器互相配合執行的,在必要時,我們還可以自定義類加載器,需要注意的是,Java虛拟機對class檔案采用的是按需加載的方式,也就是說當需要使用該類時才會将它的class檔案加載到記憶體生成class對象,而且加載某個類的class檔案時,Java虛拟機采用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面我們進一步了解它。

#了解雙親委派模式

##雙親委派模式工作原理

雙親委派模式要求除了頂層的啟動類加載器外,其餘的類加載器都應當有自己的父類加載器,請注意雙親委派模式中的父子關系并非通常所說的類繼承關系,而是采用組合關系來複用父類加載器的相關代碼,類加載器間的關系如下:

2020-09-07Class 對象Java的類加載過程

雙親委派模式是在Java 1.2後引入的,其工作原理的是,如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終将到達頂層的啟動類加載器,如果父類加載器可以完成類加載任務,就成功傳回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丢給父親去幹,直到父親說這件事我也幹不了時,兒子自己想辦法去完成,這不就是傳說中的實力坑爹啊?那麼采用這種模式有啥用呢?

##雙親委派模式優勢

采用雙親委派模式的是好處是Java類随着它的類加載器一起具備了一種帶有優先級的層次關系,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被随意替換,假設通過網絡傳遞一個名為

java.lang.Integer

的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字的類,發現該類已被加載,并不會重新加載網絡傳遞的過來的

java.lang.Integer

,而直接傳回已加載過的Integer.class,這樣便可以防止核心API庫被随意篡改。可能你會想,如果我們在classpath路徑下自定義一個名為

java.lang.SingleInterge

類(該類是胡編的)呢?該類并不存在

java.lang

中,經過雙親委托模式,傳遞到啟動類加載器中,由于父類加載器路徑下并沒有該類,是以不會加載,将反向委托給子類加載器加載,最終會通過系統類加載器加載該類。但是這樣做是不允許,因為

java.lang

是核心API包,需要通路權限,強制加載将會報出如下異常

java.lang.SecurityException: Prohibited package name: java.lang

是以無論如何都無法加載成功的。下面我們從代碼層面了解幾個Java中定義的類加載器及其雙親委派模式的實作,它們類圖關系如下

2020-09-07Class 對象Java的類加載過程

從圖可以看出頂層的類加載器是ClassLoader類,它是一個抽象類,其後所有的類加載器都繼承自ClassLoader(不包括啟動類加載器),這裡我們主要介紹ClassLoader中幾個比較重要的方法。

https://blog.csdn.net/javazejian/article/details/73413292這個部落格寫的挺好的,實際上jvm原理上面也有這些内容。