自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

類的奇幻漂流-類加載機(jī)制探秘

開發(fā) 前端
我們寫的類,在編譯完成后,究竟是怎么加載進(jìn)虛擬機(jī)的?虛擬機(jī)又做了什么神奇操作?本文可以帶著讀者初探類加載機(jī)制。上來先放類加載各個階段的主要任務(wù),用于給讀者一個大概的印象體驗,現(xiàn)在記不住也沒有什么關(guān)系。

[[374001]]

 我們寫的類,在編譯完成后,究竟是怎么加載進(jìn)虛擬機(jī)的?虛擬機(jī)又做了什么神奇操作?本文可以帶著讀者初探類加載機(jī)制。上來先放類加載各個階段的主要任務(wù),用于給讀者一個大概的印象體驗,現(xiàn)在記不住也沒有什么關(guān)系。

現(xiàn)在只需要記住三個名詞,裝載——>連接——>初始化,記住了嗎,我們要開始奇幻漂流了!

類的奇幻漂流——類加載機(jī)制探秘

在文章的最后,我們使用幾個例子來加深對程序執(zhí)行順序的理解。

1. 裝載

我覺得這里使用裝載更好一點(diǎn),第一,可以避免與類加載過程中的“加載”混淆,第二,裝載體現(xiàn)的就是一個“裝”字,僅僅是把貨物從一個地方搬到另外一個地方而已,而這里的加載,卻包含搬運(yùn)貨物、處理貨物等一系列流程。

裝載階段,將.class字節(jié)碼文件的二進(jìn)制數(shù)據(jù)讀入內(nèi)存中,然后將這些數(shù)據(jù)翻譯成類的元數(shù)據(jù),元數(shù)據(jù)包括方法代碼,變量名,方法名,訪問權(quán)限與返回值,接著將元數(shù)據(jù)存入方法區(qū)。最后會在堆中創(chuàng)建一個Class對象,用來封裝類在方法區(qū)中的數(shù)據(jù)結(jié)構(gòu),因此我們可以通過訪問此Class對象,來間接訪問方法區(qū)中的元數(shù)據(jù)。

在Java7與Java8之后,方法區(qū)有不同的實現(xiàn),這部分詳細(xì)內(nèi)容可以參考我的另外一篇博客靈性一問——為什么用元空間替換永久代?

總結(jié)來講,裝載的子流程為:

.class文件讀入內(nèi)存——>元數(shù)據(jù)放進(jìn)方法區(qū)——>Class對象放進(jìn)堆中

最后我們訪問此Class對象,即可獲取該類在方法區(qū)中的結(jié)構(gòu)。

2. 連接

連接又包括驗證、準(zhǔn)備、初始化

2.1 驗證

驗證被加載類的正確性與安全性,看class文件是否正確,是否對會對虛擬機(jī)造成安全問題等,主要去驗證文件格式、元數(shù)據(jù)、字節(jié)碼與符合引用。

2.1.1 驗證文件格式

2.1.1.1 驗證文件類型

每個文件都有特定的類型,類型標(biāo)識字段存在于文件的開頭中,采用16進(jìn)制表示,類型標(biāo)識字段稱為魔數(shù),class文件的魔數(shù)為0xCAFEBABY,關(guān)于此魔數(shù)的由來也很有意思,可以看這篇文章class文件魔數(shù)CAFEBABE的由來。

2.1.1.2 驗證主次版本號

檢查看主次版本號是否在當(dāng)前jvm處理的范圍之內(nèi),主次版本號的存放位置緊隨在魔數(shù)之后。

2.1.1.3 驗證常量池

常量池是class文件中最為復(fù)雜的一部分,對常量池的驗證主要是驗證常量池中是否有不支持的類型。

例如,有以下簡答的代碼:

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         int a=1; 
  4.         int b=2; 
  5.         int c=a+b; 
  6.     } 

 在該類的路徑下,使用javac Main.java編譯,然后使用javap -v Main可以輸出以下信息:

類的奇幻漂流——類加載機(jī)制探秘

以上標(biāo)紅處,就是class文件中存儲常量池的地方。

2.1.2 驗證元數(shù)據(jù)

主要是對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合java語言規(guī)范的要求,比如說驗證這個類是不是有父類,類中的字段方法是不是和父類沖突等等。

2.1.3 驗證字節(jié)碼

這是整個驗證過程最復(fù)雜的階段,主要是通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。

2.1.4 驗證符號引用

它是驗證的最后一個階段,發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時候。主要是對類自身以外的信息進(jìn)行校驗。目的是確保解析動作能夠完成。

對整個類加載機(jī)制而言,驗證階段是一個很重要但是非必需的階段,如果我們的代碼能夠確保沒有問題,那么就沒有必要去驗證,畢竟驗證需要花費(fèi)一定的的時間,可以使用-Xverfity:none來關(guān)閉大部分的驗證。

2.2 準(zhǔn)備

在這個階段中,主要是為類變量(靜態(tài)變量)分配內(nèi)存以及初始化默認(rèn)值,因為靜態(tài)變量全局只有一份,是跟著類走的,因此分配內(nèi)存其實是在方法區(qū)上分配。

這里有3個注意點(diǎn):

(1)在準(zhǔn)備階段,虛擬機(jī)只為靜態(tài)變量分配內(nèi)存,實例變量要等到初始化階段才開始分配內(nèi)存。這個時候還沒有實例化該類,連對象都沒有,因此這個時候還不存在實例變量。

(2)為靜態(tài)變量初始化默認(rèn)值,注意,是初始化對應(yīng)數(shù)據(jù)類型的默認(rèn)值,不是自定義的值。

例如,代碼中是這樣寫的,自定義int類型的變量a的值為1

  1. private static int a=1; 

但是,在準(zhǔn)備階段完成之后,a的值只會被初始化為0,而不是1。

(3)被final修飾的靜態(tài)變量,如果值比較小,則在編譯后直接內(nèi)嵌到字節(jié)碼中。如果值比較大,也是在編譯后直接放入常量池中。因此,準(zhǔn)備階段結(jié)束后,final類型的靜態(tài)變量已經(jīng)有了用戶自定義的值,而不是默認(rèn)值。

2.3 解析

解析階段,主要是將class文件中常量池中的符號引用轉(zhuǎn)化為直接引用。

符號引用的含義:

可以直接理解為是一個字符串,用這個字符串來表示一個目標(biāo)。就像博主的名字是SunAlwaysOnline,這個SunAlwaysOnline字符串就是一個符號引用,代表博主,但是現(xiàn)在不能通過名字直接找到我本人。

直接引用的含義:

直接引用是一個指向目標(biāo)的指針,能夠通過直接引用定位到目標(biāo)。比如

  1. Student s=new Student(); 

我們可以通過引用變量s直接定位到新創(chuàng)建出的Student對象實例。

將符號引用轉(zhuǎn)化為直接引用,就能將平淡無奇的字符串轉(zhuǎn)化為指向?qū)ο蟮闹羔槨?/p>

3. 初始化

執(zhí)行初始化,就是虛擬機(jī)執(zhí)行類構(gòu)造器()方法的過程,()方法是由編譯器自動去搜集類中的所有類變量與靜態(tài)語句塊合并產(chǎn)生的??赡艽嬖诙鄠€線程同時執(zhí)行某個類的()方法,虛擬機(jī)此時會對該方法進(jìn)行加鎖,保證只有一個線程能執(zhí)行。

到了這個階段,類變量與類成員變量才會被賦予用戶自定義的值。

當(dāng)然,一個類并不是被初始化多次,只有當(dāng)對類的首次主動使用的時候才會導(dǎo)致類的初始化。主動使用包含以下幾種方式:

  • 使用new語句創(chuàng)建類的對象訪問類靜態(tài)變量,或者對該靜態(tài)變量賦值調(diào)用類的靜態(tài)方法通過反射方式獲取對象實例有public static void main(String[] args)方法的類會首先被初始化初始化一個類時,如果父類還沒有被初始化,則首先會初始化父類,再初始化該類。

被動使用會發(fā)生呢?

  • 當(dāng)訪問一個靜態(tài)變量時時,只有真正聲明這個靜態(tài)變量的類才會被初始化。例如:通過子類引用父類的靜態(tài)變量,不會導(dǎo)致子類初始化。引用常量不會觸發(fā)此類的初始化(常量在編譯階段就內(nèi)嵌進(jìn)字節(jié)碼或存入調(diào)用類的常量池中)聲明并創(chuàng)建數(shù)組時,不會觸發(fā)類的初始化。例如Student array=new Student[2];

4. 類的初始化順序

現(xiàn)在有以下的代碼:

  1. class Father { 
  2.  
  3.     public static int fatherA = 1; 
  4.     public static final int fatherB = 2; 
  5.  
  6.     static { 
  7.         System.out.println("父類的靜態(tài)代碼塊"); 
  8.     } 
  9.  
  10.     { 
  11.         System.out.println("父類的非靜態(tài)代碼塊"); 
  12.     } 
  13.  
  14.     Father() { 
  15.         System.out.println("父類的構(gòu)造方法"); 
  16.     } 
  17.  
  18. class Son extends Father { 
  19.     public static int sonA = 3; 
  20.     public static final int sonB = 4; 
  21.  
  22.     static { 
  23.         System.out.println("子類的靜態(tài)代碼塊"); 
  24.     } 
  25.  
  26.     { 
  27.         System.out.println("子類的非靜態(tài)代碼塊"); 
  28.     } 
  29.  
  30.     Son() { 
  31.         System.out.println("子類的構(gòu)造方法"); 
  32.     } 

 (1)Main方法中實例化子類:

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         Son son = new Son(); 
  4.     } 

 首先可以確定的是,這屬于主動使用,父類先于子類初始化,因此會得到以下的輸出:

類的奇幻漂流——類加載機(jī)制探秘

這里可以進(jìn)行總結(jié),程序執(zhí)行的順序為:

父類的靜態(tài)域->子類的靜態(tài)域->父類的非靜態(tài)域->子類的非靜態(tài)域->父類的構(gòu)造方法->子類的構(gòu)造方法

這里的靜態(tài)域包括靜態(tài)變量與靜態(tài)代碼塊,靜態(tài)變量和靜態(tài)代碼塊的執(zhí)行順序由編碼順序決定。

規(guī)律就是,靜態(tài)先于非靜態(tài),父類先于子類,構(gòu)造方法在最后。嗯給我背三遍

(2)Mian方法中輸出子類的sonA屬性

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         System.out.println(Son.sonA); 
  4.     } 

 這里只要輸出子類的靜態(tài)屬性sonA,因此需要初始化子類,但父類還沒有被初始化,因此先初始化父類。一般而言,靜態(tài)代碼塊會對靜態(tài)變量進(jìn)行賦值,因此調(diào)用靜態(tài)屬性,在此之前虛擬機(jī)會調(diào)用靜態(tài)代碼塊。所以,輸出如下:

類的奇幻漂流——類加載機(jī)制探秘

(3)Main方法輸出子類繼承而來的fatherA屬性

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         System.out.println(Son.fatherA); 
  4.     } 

 子類從父類繼承而來的屬性,因此這里屬于被動使用。只會執(zhí)行靜態(tài)屬性真正存在的那個類的初始化,即只會初始化父類。因此,輸出:

類的奇幻漂流——類加載機(jī)制探秘

(4)Main方法中聲明并創(chuàng)建一個子類類型的數(shù)組

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.        Son[] sons=new Son[10]; 
  4.     } 

 顯然,這屬于被動使用,不會初始化Son類。因此,沒有任何輸出。

(5)Main方法輸出子類被static final修飾的變量

  1. public class Main { 
  2.     public static void main(String[] args) { 
  3.         System.out.println(Son.sonB); 
  4.     } 

 顯然,被static final修改的變量,也就是一個常量,在編譯器就放入類的常量池中了,不需要初始化類。因此,這里只輸出sonB的值,即為4。

(6)在聲明前使用一個靜態(tài)變量

  1. public class Main { 
  2.     static { 
  3.         c = 1; 
  4.     } 
  5.  
  6.     public static int c; 

這樣的代碼,是可以運(yùn)行的,小朋友,你是不是有大大的疑問?但容我自仔細(xì)分析來。

首先,在準(zhǔn)備階段,為靜態(tài)變量c分配內(nèi)存,然后賦予初始值0。等到初始化階段,執(zhí)行類的靜態(tài)域,也就是執(zhí)行此處的靜態(tài)代碼塊中c=1,c此時已經(jīng)存在,也有了一個默認(rèn)值0,此時可以修改c的值。

但是,如果我僅僅在c=1后使用c的話,如:

  1. public class Main { 
  2.     static { 
  3.         c = 1; 
  4.         System.out.println(c); 
  5.     } 
  6.  
  7.     public static int c; 

 此時編譯沒法通過,編輯器提示Illegal forward reference,即非法前向引用,似乎只能寫入c,不能讀取c。我們之前已經(jīng)分析過了,此時在內(nèi)存中是有這個c的,那為什么不能讀取c?

本來在正常的情況下,要想使用一個變量,變量首先需要聲明出來。當(dāng)然,java做出了一種特許,允許在使用前不先聲明,但必須要滿足幾個條件,其中有一個條件是該變量只能出現(xiàn)在賦值表達(dá)式的左邊,即c=1可以,c=2可以,c+=1不可以(c+=1也就是c=c+1,違反了左值協(xié)定)。當(dāng)然如果這里使用全限定名,也就是輸出Main.c時,則可以正常運(yùn)行。

有的小伙伴可能還是有大大的疑問,不要緊,沒看懂的可以參考以下講解非法前向引用的文章

java報錯非法的前向引用問題

Java編譯時提示非法向前引用

Illegal forward Reference java issue

關(guān)于加載使用到的類加載器,雙親委派機(jī)制,如何自定義類加載器,可能需要另開篇幅。

 

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2021-01-06 09:51:19

類加載器雙親委派模型

2012-12-19 09:36:33

2021-07-05 06:51:43

Java機(jī)制類加載器

2024-09-06 09:37:45

WebApp類加載器Web 應(yīng)用

2024-03-12 07:44:53

JVM雙親委托機(jī)制類加載器

2023-10-31 16:00:51

類加載機(jī)制Java

2017-09-20 08:07:32

java加載機(jī)制

2021-04-29 11:18:14

JVM加載機(jī)制

2017-03-08 10:30:43

JVMJava加載機(jī)制

2024-09-04 09:47:21

2009-02-03 09:42:53

JAVA類JVM指令forName方法

2025-02-05 11:43:28

2024-12-02 09:01:23

Java虛擬機(jī)內(nèi)存

2020-10-26 11:20:04

jvm類加載Java

2023-05-10 11:07:18

2023-10-17 09:26:44

Java工具

2022-10-08 08:34:34

JVM加載機(jī)制代碼

2012-01-18 11:24:18

Java

2020-05-20 22:13:26

JVM加載機(jī)制虛擬機(jī)

2012-02-09 10:31:17

Java
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號