《深刻理解Java虛擬機》--類加載與對象的探究

類的加載

Java虛擬機規範中,沒有強制約束何時要開始加載,可是,卻嚴格規定了幾種狀況java

必須進行初始化(加載,驗證,準備則須要在初始化以前開始):面試

  • 遇到 new、getstatic、putstatic、或者invokestatic 這4條字節碼指令,若是沒有類沒有進行過初始化,則觸發初始化。
  • 使用java.lang.reflect包的方法,對壘進行反射調用的時候,若是沒有初始化,則先觸發初始化
  • 初始化一個類時候,若是發現父類沒有初始化,則先觸發父類的初始化。


類從被加載到虛擬機內存開始,直到卸載出內存爲止,它的整個生命週期包括:算法

加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中,驗證、準備和解析統稱爲鏈接(Linking)。過程以下圖所示:
緩存


加載安全

加載階段會作3件事情:數據結構

  • 經過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。

相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。框架

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在Java堆中也建立一個java.lang.Class類的對象,這樣即可以經過該對象訪問方法區中的這些數據。
ide

驗證佈局

驗證類數據信息是否符合JVM規範,是不是一個有效的字節碼文件,驗證內容涵蓋了類數據信息的格式驗證、語義分析、操做驗證等。
spa

準備

爲類的靜態變量分配內存,並初始化默認值,這些內存是在方法區中分配,須要注意如下幾點:

  • 此處內存分配的變量僅包含類變量(static),而不包括實例變量,實例變量會隨着對象實例化被分配在java堆中。
  • 這裏默認值是數據類型的默認值(如0、0L、null、false),而不是代碼中被顯示的賦予的值。
  • 若是類字段的字段屬性表中存在ConstatntValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,能夠是任何字面量。

直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

初始化

初始化算是類加載過程的最後一個階段,在這個階段在是真正的開始有java代碼主導。將一個類中全部被static關鍵字標識的代碼統一執行一遍,若是執行的是靜態變量,那麼就會使用用戶指定的值覆蓋以前在準備階段設置的初始值;若是執行的是static代碼塊,那麼在初始化階段,JVM就會執行static代碼塊中定義的全部操做


類加載器

類加載器除了能用來加載類,還能用來做爲類的層次劃分。Java自身提供了3種類加載器

  • 啓動類加載器(Bootstrap ClassLoader),它是屬於虛擬機自身的一部分,用C++實現的,主要負責加載

<JAVA_HOME>\lib目錄中或被-Xbootclasspath指定的路徑中的而且文件名是被虛擬機識別的文件。它等因而全部類加載器的爸爸。

  • 擴展類加載器(Extension ClassLoader),它是Java實現的,獨立於虛擬機,主要負責加載<JAVA_HOME>\lib\ext目錄中或被java.ext.dirs系統變量所指定的路徑的類庫。
  • 應用程序類加載器(Application ClassLoader),它是Java實現的,獨立於虛擬機。主要負責加載用戶類路徑(classPath)上的類庫,若是咱們沒有實現自定義的類加載器那這玩意就是咱們程序中的默認加載器。

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。

比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。


雙親委派模型

                              

上面圖片所展現的類加載器之間的這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼。

雙親委派模型的工做過程

若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自 己去加載。 

使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一塊兒具有了一種帶有優先級的層次關係。

例如類java.lang.Object,它存放在 rt.jar之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,所以Object類在程序的各類類加載器環境中都是同一個類。相反,若是沒有使用雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身編寫了一個稱爲 java.lang.Object的類,並放在程序的ClassPath中,那系統中將會出現多個不一樣的Object 類,Java類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂。

破壞雙親委派模型:

你先得知道SPI(Service Provider Interface),它和API不同,它是面向拓展的,也就是我定義了這個SPI,具體如何實現由擴展者實現。我就是定了個規矩。

Java弄了個線程上下文類加載器,經過setContextClassLoader()默認狀況就是應用程序類加載器而後Thread.current.currentThread().getContextClassLoader()得到類加載器來加載。

Java中全部涉及SPI(Service Provider Interface)的加載動做基本上都採用這種方式(線程上下文類加載器,能夠作一些「舞弊」的事情了,JNDI服務使用這個線程上下文類加載器去加載所須要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動 做,),例如JNDI、JDBC、JCE、JAXB和JBI等。


對象的建立

建立對象(克隆、反序列化)通常是一個newkeyword而已,而在虛擬機中,對象的建立步驟例如如下: 

①當虛擬機遇到new指令時。首先將去檢查這個指令參數可否在常量池中定位到一個類的引用符號,並且檢查這個符號引用表明的類是否被載入、解析和初始化過。假設沒有。那必須先執行相應的 類載入過程

②在類載入檢查經過之後。接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類載入後便肯定。爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆劃分出來

  ②.①建立對象的過程其實也是一個非線程安全的過程,因此也須要考慮線程安全的問題。可能出現正在給對象A分配內存,指針還沒來得及改動,對象B又同一時候使用了原來的指針來分配內存的狀況。解決這一問題的方案是: 

  •   方案1、對分配內存空間的動做進行同步處理--實際上虛擬機採用CAS配上失敗重試的方式,保證更新操做原子性 。
  • 方案2、把內存分配的動做依照線程劃分在不一樣空間之中進行。即每個線程在Java堆中預先分配一小塊內存。稱爲本地線程分配緩存(TLAB)。哪一個線程要分配內存,就在哪一個線程的TLAB上分配,僅僅有TLAB用完並分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以經過-XX:+/-UseTLAB參數來設定。

③內存分配完畢之後。虛擬機會將分配到的內存空間都初始化爲零值(不包括對象頭),假設使用TLAB,這一工做過程也可以提早至TLAB分配時進行,這一步操做保證了對象實例字段在Java代碼中可以不賦初始值就能直接使用,程序能訪問到這些字段的數據類型所相應的零值

④接下來虛擬機要對對象進行必要的設置,好比:這個對象是哪一個類的實例、怎樣才幹找到類的元數據信息、對象的哈希碼、對象GC分代年齡信息等。這些信息存放在對象的信息頭之中。依據虛擬機執行狀態的不一樣。如是否使用偏向鎖等,對象頭會有不一樣的設置方式。 


上述工做完畢之後,從虛擬機角度來看,一個新的對象已經產生了,但是從Java程序來看,對象纔剛剛開始——(init)方法尚未執行。所有的字段都還爲零,因此,通常來講。執行new命令後。會接着執行init方法。把對象依照程序猿的意願進行初始化,這樣一個真正可用的對象纔算全然產生出來。 


對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:

  • 對象頭(Header)
  • 實例數據(Instance Data)
  • 對齊填充(Padding)

HotSpot虛擬機的對象頭包括兩部分信息:

第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、 GC分代年齡、 鎖狀態標誌、 線程持有的鎖、 偏向線程ID、 偏向時間戳等,這部分數據稱爲Mark Word。  

對象頭的另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。

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

對齊填充:對齊填充並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用。


對象的訪問定位

使用對象時Java程序須要經過棧上的reference數據來操做堆上的具體對象。

目前主流的訪問方式有使用句柄直接指針兩種

  • 若是使用句柄訪問的話,Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。   如圖所示:


  • 若是使用直接指針訪問,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址,如圖所示:



各自優點:

使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不須要修改。

使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷, 因爲對象的訪問在Java中很是頻繁,所以這類開銷聚沙成塔後也是一項很是可觀的執行成 本。就本書討論的主要虛擬機Sun HotSpot而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各類語言和框架使用句柄來訪問的狀況也十分常見。

對象是否「已死」

引用計數算法:

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。

至少主流的Java虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。(通常面試問和教科書上的解釋的都是這個。)

可達性分析算法:

在主流程序語言(Java、C#)的主流實現中,都是稱經過可達性分析(Reachability Analysis)來斷定對象是否存活的。這個算法的基本思路就是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。

即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機調用過,那麼它們就會被行刑(清除)。

相關文章
相關標籤/搜索