<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊這道經(jīng)典的大廠面試題

          共 9590字,需瀏覽 20分鐘

           ·

          2022-02-20 11:03


          大家好,今天我們來學(xué)習(xí)一道 Java 常見的面試題:類加載機(jī)制。


          在工程中我們基本無時(shí)無刻都在和對(duì)象打交道,那么大家有想過這些這些對(duì)象是怎么來的嗎,當(dāng) new 一個(gè)對(duì)象的時(shí)候到底發(fā)生了什么?

          沒錯(cuò),就是類加載機(jī)制,了解這個(gè)機(jī)制很重要,這不僅能讓我們理解 JVM 的運(yùn)行機(jī)制,更重要的是它還能解釋一些我們看起來覺得很奇怪的現(xiàn)象,比如如下懶漢式單例模式

          public?class?Singleton?{
          ??private?Singleton()?{}
          ??private?static?class?LazyHolder?{
          ????static?final?Singleton?INSTANCE?=?new?Singleton();
          ??}
          ??public?static?Singleton?getInstance()?{
          ????return?LazyHolder.INSTANCE;
          ??}
          }

          乍一看可能會(huì)覺得多線程環(huán)境下可能會(huì)產(chǎn)生多個(gè) Singleton 實(shí)例,實(shí)際上由于類初始化是線程安全的,并且僅被執(zhí)行一次,因此程序可以確保多線程環(huán)境下有且僅有一個(gè) Singleton 實(shí)例,再問這個(gè)線程安全是如何保證的?這就得進(jìn)一步了解類初始化階段的 clinit 方法了,所以你看看了解類加載這些底層的機(jī)制有多么重要。

          本文思維導(dǎo)圖如下:

          類加載機(jī)制簡介

          類加載整體流程如下圖所示,這也是類的生命周期

          我們可以看到,字節(jié)碼文件需要經(jīng)過加載鏈接(包括驗(yàn)證,準(zhǔn)備,解析),初始化才能轉(zhuǎn)為類,然后才能根據(jù)類來創(chuàng)建對(duì)象

          需要注意的是,圖中紅框所代表的加載驗(yàn)證準(zhǔn)備初始化卸載這五個(gè)階段的順序是確定的,類加載必須嚴(yán)格按照這五個(gè)階段的順序來開始,但解析階段則未必,有可能在初始化之后才開始,主要是為了支持 Java 的動(dòng)態(tài)綁定特性,那么各個(gè)階段主要做了哪些事呢

          加載

          在加載階段,虛擬機(jī)需要完成以下三件事

          1. 通過一個(gè)類的全限定名來獲取此類的二進(jìn)制字節(jié)流

          2. 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)結(jié)構(gòu)

          3. 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口

          加載

          如上圖所示,加載后生成的類對(duì)象與對(duì)象間的關(guān)系如上圖所示,什么是類對(duì)象呢,比如實(shí)例的 getClass() 或 Foo.Class 即為類對(duì)象

          每個(gè)類只有一個(gè)對(duì)象實(shí)例(類對(duì)象),多個(gè)對(duì)象共享類對(duì)象,這里有個(gè)需要注意的點(diǎn)是類對(duì)象是在堆中而不是在方法區(qū)(這里針對(duì)的是 Java 7 及以后的版本),所有的對(duì)象都是分配在堆中的,類對(duì)象也是對(duì)象,所以也是分配在堆中,這點(diǎn)網(wǎng)上挺多人混淆了,需要注意一下

          有人可能會(huì)奇怪,只看上面這張圖,對(duì)象和類對(duì)象貌似聯(lián)系不起來,實(shí)際上在虛擬機(jī)底層,比如 Java Hotspot 虛擬機(jī),對(duì)象和類是以一種被稱為 oop-klass 的模型來表示的,每個(gè)對(duì)象或類都有對(duì)應(yīng)的 C++ 類表示方式,它的底層其實(shí)是如下這樣來表示的,通過下圖可以看到,通過這種方式實(shí)例對(duì)象和 Class 對(duì)象就能聯(lián)系起來了,我們另一篇講述對(duì)象模型時(shí)再詳述 oop-klass 對(duì)象,這里先按下不表

          類元信息也就是類的信息主要分配在方法區(qū),在 Java 8 中方法區(qū)是在元空間(metaspace)中實(shí)現(xiàn)的,所以類元信息是保存在元空間的。

          注意這一階段雖然名曰加載,但其實(shí)在加載階段是夾雜著一些驗(yàn)證工作的,主要有以下驗(yàn)證

          • 文件格式的驗(yàn)證:比如驗(yàn)證字節(jié)碼是否是以魔數(shù) 0xCAFEBABE 開頭,主次版本是否在當(dāng)前虛擬機(jī)可接受范圍內(nèi)等安全校驗(yàn)的操作等,通過這一階段的驗(yàn)證后加載的字節(jié)流才被允許進(jìn)入 Java 虛擬機(jī)內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ)。

          • 元數(shù)據(jù)校驗(yàn):這一階段主要是對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,如確保每一個(gè)加載的類除了 Object 外都有父類,這也就意味著,一旦某個(gè)類被加載,那么它的父類,祖先類。。。等也會(huì)被加載(但此時(shí)還不會(huì)被鏈接,初始化)

          有人可能會(huì)困惑,為啥需要做這些校驗(yàn)工作呢,字節(jié)碼文件難道不安全?字節(jié)碼文件一般來說是通過正常的 Java 編譯器編譯而成的,但字節(jié)碼文件也是可以編輯修改的,也是有可能被篡改注入惡意的字節(jié)碼的,就會(huì)對(duì)程序造成不可預(yù)知的風(fēng)險(xiǎn),所以加載階段的驗(yàn)證是非常有必要的

          我們可以在執(zhí)行 java 程序的時(shí)候加上?-verbose:class?或?-XX:+TraceClassLoading?這兩個(gè) JVM 參數(shù)來觀察一下類的加載情況,比如我們寫了如下測試類

          public?class?Test?{
          ????public?static?void?main(String[]?args)?{
          ????}
          }

          編譯后執(zhí)行?java -XX:+TraceClassLoading Test

          可以看到如下加載過程

          [Opened?/Library/Internet?Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib/rt.jar]
          [Loaded?java.lang.Object?from?/Library/Internet?Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib/rt.jar]
          [Loaded?java.lang.CharSequence?from?/Library/Internet?Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib/rt.jar]
          [Loaded?java.lang.String?from?/Library/Internet?Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib/rt.jar]
          ...?//?省略號(hào)表示加載了很多?lib/rt.jar?下的類
          [Loaded?Test?from?file:/Users/ronaldo/practice/]
          ...

          注意看倒數(shù)第二行,可以看到 Test 類被加載了,這可以理解,因?yàn)閳?zhí)行了 Test 的 main 方法,Test 會(huì)被初始化,也就會(huì)被加載(之后會(huì)講述初始化條件), 但上述有挺多加載 lib/rt.jar 下的類又是怎么回事呢?

          要回答這個(gè)問題,我們必須得先搞清楚一個(gè)問題:我們說的類加載到底是由誰執(zhí)行的?

          雙親委派模式

          類加載必須由類加載器(classloader)來完成,類加載器+類的全限定名(包名+類名)唯一確定一個(gè)類,看到這有人可能會(huì)問了,類加載器難道會(huì)有多個(gè)?

          猜得沒錯(cuò)!類加載器的確會(huì)有多個(gè),為啥會(huì)有多個(gè)呢,主要有兩個(gè)目的:安全性責(zé)任分離

          首先說安全性,試想如果只有一個(gè)類加載器會(huì)出現(xiàn)什么情況,我們可能會(huì)定義一個(gè)java.lang.virus 的類,這樣的話由于此類與 Java.lang.String 等核心類處于同一個(gè)包名下,那么此類就具有訪問這些核心類 package 方法的權(quán)限,此外如果用戶自定義一個(gè) java.lang.String 類,如果類加載器加載了這個(gè)類,有可能把原本的 ?String 類給替換掉,這顯然會(huì)造成極大的安全隱患

          再來說責(zé)任分離,像 rt.jar 包下的核心類等沒有什么特殊的要求顯然可以直接加載,而且由于是核心類,程序一啟動(dòng)就會(huì)被加載,也可以進(jìn)一步優(yōu)化來提升加載速度,而有些字節(jié)碼文件由于反編譯等原因可能需要加密,此時(shí)類加載器就需要在加載字節(jié)碼文件時(shí)對(duì)其進(jìn)行解密,再比如實(shí)現(xiàn)熱部署也需要類加載器從指定的目錄中加載文件,這些功能如果都在一個(gè)類加載器里實(shí)現(xiàn),會(huì)導(dǎo)致類加載器的功能很重,所以解決辦法就是定義多個(gè)類加載器,各自負(fù)責(zé)加載指定路徑下的字節(jié)碼文件,從而針對(duì)指定路徑下的類文件加載做相關(guān)的操作,達(dá)到責(zé)任分離的目的

          在 JVM 中有哪些類加載器呢

          主要有以下三類加載器

          1. 啟動(dòng)類加載器(BootstrapClassLoader):,負(fù)責(zé)加載\lib 下的 rt.jar,resources.jar 等核心類庫或者 -Xbootclasspath 指定的文件

          2. 擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)加載\lib\ext目錄或java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫。

          3. 應(yīng)用程序類加載器(Application ClassLoader)。負(fù)責(zé)加載用戶類路徑(classpath)上的指定類庫,我們可以直接使用這個(gè)類加載器。一般情況,如果我們沒有自定義類加載器默認(rèn)就是用這個(gè)加載器。

          類加載器的主要作用就是負(fù)責(zé)加載字節(jié)碼二進(jìn)制流,將其最終轉(zhuǎn)成方法區(qū)中的類對(duì)象

          現(xiàn)在我們知道了有以上幾個(gè)種類的類加載器,那么這里有三個(gè)問題需要回答:

          1. 怎么指定類由指定的類加載器加載的呢?

          2. 類加載器是如何保證類的一致性的,由以上可知類加載器+類的全限定名唯一確定一個(gè)類,那怎么避免一個(gè)類被多個(gè)類加載器加載呢,畢竟你無法想象工程中有兩個(gè) Object 類,那豈不亂套了

          3. 類加載器(java.lang.ClassLoader)是用來加載類的,但其本身也是類,那么類加載器又是被誰加載的呢

          為了解決上述問題,類加載器采用了雙親委派模型模式來設(shè)計(jì)類加載器的層次結(jié)構(gòu)

          什么是雙親委派模式

          先來看一下雙親委派模式的整體設(shè)計(jì)架構(gòu)圖

          可以看到,程序默認(rèn)是由 AppClassLoader 加載的,每個(gè)類被相應(yīng)的加載器加載后都會(huì)被緩存起來,這樣下次再碰到相關(guān)的類直接從緩存里獲取即可,避免了重復(fù)的加載,同時(shí)每個(gè)類由于只會(huì)被相應(yīng)的類加載器加載,確保了類的唯一性,比如 java.lang.Object 只會(huì)被 BootstrapClassLoader 加載,保證了 Object 的唯一性

          類加載器是如何加載類的呢?

          1. 當(dāng)類首次被加載時(shí)(假設(shè)此類為 ArrayList),AppClassLoader 并不會(huì)馬上就加載它,而是會(huì)向上委托給它的 parent,即 ExtClassLoader,查看是否已加載了這個(gè)類,如果沒有則繼續(xù)向上委托給 BootsrapClassLoader 讓其加載,此時(shí) BootsrapClassLoader 就會(huì)從 lib/rt.jar 加載此類生成類對(duì)象并緩存起來,然后 BootsrapClassLoader 會(huì)把此類對(duì)象返回給 ExtClassLoader,ExtClassLoader 再把此類對(duì)象返回給 AppClassLoader,然后就可以基于此類對(duì)象來創(chuàng)建類的實(shí)例對(duì)象了

          2. 當(dāng)再次調(diào)用 new ArrayList() 時(shí),也會(huì)觸發(fā) ArrayList 的加載,此時(shí) AppClassLoader 也會(huì)首先往上層層委托給 BootsrapClassLoader 給加載,由于其緩存里已經(jīng)有此類對(duì)象了,所以直接在緩存里查找后遞歸返回給 AppClassLoader 即可。

          再來看上述問題 3,類加載器是被誰加載的?

          實(shí)際上 AppClassLoader 和 ExClassLoader 都是 java.lang.ClassLoader 的子類,它們都是在應(yīng)用啟動(dòng)時(shí)是由 BootstrapClassLoader 加載的,畢竟其它類要由這三個(gè)類加載器加載,所以這三個(gè)類加載器必須先存在,那么誰來加載 BootstrapClassLoader 呢,如果還是由另一個(gè)類加載器加載,那么還要設(shè)計(jì)一個(gè)類加載器來加載它,。。。,就陷入了無限循環(huán)之中,所以 BootstrapClassLoader 在 JVM 中本身是以 C++ 形式實(shí)現(xiàn)的,是 JVM 的一部分,在應(yīng)用啟動(dòng)時(shí)就存在了,所以它本身可以說是 JVM 自身創(chuàng)建的,不需要由另外的加載器加載,所以它也被稱為根加載器

          java.lang 下的一些核心類如 Object,String,Class 等核心類本身非常重要也很常用,所以在應(yīng)用啟動(dòng)時(shí) BootstrapClassLoader 也會(huì)提前把它們加載好,另外在加載 AppClassLoader 和 ExClassLoader 時(shí)在這兩個(gè)類中也會(huì)遇到使用 List 等核心類的情況,所以也會(huì)把 rt.jar 中的這些核心類也一起加載了,這就是為什么我們?cè)谏衔目吹?Test 類被加載前也看到了這些核心類被加載的原因

          類加載都要遵循雙親委派機(jī)制嗎

          不是的,一個(gè)典型的應(yīng)用場景就是 Tomcat 的類加載,由于 Tomcat 可能會(huì)加載多個(gè) web 應(yīng)用,而多個(gè)應(yīng)用很有可能出現(xiàn)包名+類名都一樣的類,最典型的比如兩個(gè)應(yīng)用采用了同樣的第三方類庫,但是它們的版本不同,這種情況下如果按雙親委派來加載,只會(huì)有一個(gè)類對(duì)象,顯然有問題,這種情況要能區(qū)分各個(gè)應(yīng)用的類,就得破壞雙親委派機(jī)制,如下:

          綠色部分是 java 項(xiàng)目在打 war 包的時(shí)候, tomcat 自動(dòng)生成的類加載器, 也就是說 , 每一個(gè)項(xiàng)目打成一個(gè)war包, tomcat都會(huì)自動(dòng)生成一個(gè)類加載器, 專門用來加載這個(gè) war 包,當(dāng)加載 war 包中的類時(shí),首先由 webappClassLoader 加載,而不是首先委托給上一級(jí)類加載器加載,這樣的話由于加載每一個(gè) war 包的 webappClassLoader 都不一樣,每個(gè) war 包被加載的類最終生成的類對(duì)象也必然不一樣!就達(dá)到了應(yīng)用程序間類的隔離

          最后有一個(gè)需要注意的點(diǎn)是并不是所有的類都需要通過類加載器來加載創(chuàng)建,比如數(shù)組類就比較特殊,它是由 Java 虛擬機(jī)直接在內(nèi)存中動(dòng)態(tài)構(gòu)造出來的,但由于類的特性(類加載器+類的全限定名惟一確定一個(gè)類),數(shù)組類依然最終會(huì)會(huì)被標(biāo)識(shí)在某個(gè)加載器的命名空間下,到底標(biāo)識(shí)在哪個(gè)類加載器的命名空間下,取決于數(shù)組的組件類型(比如 int[] 數(shù)組組件類型為 int,String[] 數(shù)組組件類型為 String),如果組件類型為 int 等基本類型,會(huì)標(biāo)識(shí)在啟動(dòng)類加載器 bootstrapclassloader 下,如果為其它的引用類型(比如自定義的類 Test,數(shù)組為 Test)則標(biāo)識(shí)為最終加載此類的類加載器下

          花了這么大的筆墨終于把加載階段講完了,這個(gè)階段真的很重要,不僅是因?yàn)樗穷惣虞d的第一個(gè)階段,還因?yàn)槠渲猩婕暗诫p親委派等原理,如果沒有搞明白的,建議多看幾遍,應(yīng)該都講得比較清楚了。

          接下來我們?cè)賮砜戳硗鈨蓚€(gè)階段:鏈接初始化,首先需要明白的是,加載階段完成后并不會(huì)馬上就做之后的鏈接,初始化的操作,比如如果我有一個(gè)類 Test,在方法中定義了一個(gè)?Test[] list = new Test[10];?這樣的數(shù)組變量,此時(shí)會(huì)觸發(fā) Test 類的加載,但并不會(huì)觸發(fā) Test 類的鏈接,初始化。那么什么是鏈接和初始化呢

          鏈接

          鏈接包括三個(gè)階段:

          驗(yàn)證準(zhǔn)備解析,其中驗(yàn)證又包括字節(jié)碼驗(yàn)證符號(hào)引用驗(yàn)證

          這里的驗(yàn)證主要有兩種字節(jié)碼驗(yàn)證符號(hào)引用驗(yàn)證

          字節(jié)碼驗(yàn)證

          這個(gè)階段主要是對(duì)類的方法體(Class 文件中的 Code 屬性)進(jìn)行校驗(yàn)分析,保證被校驗(yàn)類的方法不會(huì)在運(yùn)行時(shí)做出危害虛擬機(jī)安全的行為,比如:

          • 保證任何跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上

          • 保證類的轉(zhuǎn)換是有效的,比如可以把子類對(duì)象賦值給父類變量,反之則不行

          符號(hào)引用驗(yàn)證

          這個(gè)驗(yàn)證其實(shí)是在解析階段發(fā)生,符號(hào)引用可以看作是對(duì)類自身以外(常用池引用中的各種符合引用)的各類信息進(jìn)行匹配性的驗(yàn)證,我們知道在字節(jié)碼方法中如果調(diào)用了或者說引用了某個(gè)類,那么這個(gè)類是在字節(jié)碼中是以符號(hào)引用的形式存在的,所以就要確保真正用到此類的時(shí)候能找到此類,如果找不到就會(huì)報(bào)錯(cuò),舉個(gè)簡單的例子,假設(shè)有以下兩個(gè)類,顯然編譯時(shí)都能通過,但在編譯后如果我把 B.class 刪掉,A.class 保留著 B 類的符號(hào)引用,如果執(zhí)行 A 的 main 方法需要加載 B 類,由于 B.class 文件缺失導(dǎo)致無法加載 B 類,就會(huì)報(bào)錯(cuò)

          //?B.java
          public?class?B?{
          }

          //?A.java
          public?class?A?{
          ????public?static?void?main(String[]?args)?{
          ????????B?b?=?new?B();
          ????}
          }

          符號(hào)引用驗(yàn)證不光驗(yàn)證類,還會(huì)驗(yàn)證方法,字段等

          注意,類的驗(yàn)證并不是必須的,如果你能確保你的 class 文件是絕對(duì)安全的,那么可以開啟 -Xverify:none 來關(guān)閉類的驗(yàn)證,這樣可以縮短類的加載時(shí)間以達(dá)到加快類加載的目的。

          準(zhǔn)備

          準(zhǔn)備階段的主要目的有兩個(gè)

          1. 為了給被加載類的靜態(tài)字段分配內(nèi)存,并為其賦默認(rèn)初始值,如 int 類型的靜態(tài)變量默認(rèn)賦值為 0

          2. 部分 Java 虛擬機(jī)還會(huì)在此階段構(gòu)造其他跟類層次相關(guān)的數(shù)據(jù)結(jié)構(gòu),比如說用來實(shí)現(xiàn)虛方法的動(dòng)態(tài)綁定的方法表。

          解析

          如前所述,這一階段會(huì)進(jìn)行符號(hào)引用驗(yàn)證,主要作用是在運(yùn)行時(shí)把字節(jié)碼類中的常量池符號(hào)引用解析成為能定位到內(nèi)存方法區(qū)中對(duì)應(yīng)類信息的直接引用(內(nèi)存中的具體地址),以上述的代碼為例

          //?B.java

          public?class?B?{

          }

          //?A.java

          public?class?A?{
          ????public?static?void?main(String[]?args)?{
          ????????????B?b?=?new?B();
          ????}
          }

          在編譯后,A 類的字節(jié)碼文件 A.class 包括 B 的符號(hào)引用,那么在執(zhí)行 main 方法后,由于碰到了?new B(),此時(shí)就會(huì)將 B 的符號(hào)引用轉(zhuǎn)為指向 B 的類對(duì)象的直接引用,由于 B 未加載,所以,所以此時(shí)也會(huì)觸發(fā) B 的加載生成 B 的類對(duì)象,這樣符號(hào)引用就可以轉(zhuǎn)成直接引用了,這里是以類的解析舉例,但實(shí)際上,常量,方法,字段等符號(hào)引用也都會(huì)被解析

          但需要注意的是這一階段有可能發(fā)生在初始化之后,因?yàn)橹挥姓嬲玫搅吮热缧枰{(diào)用某個(gè)類的方法時(shí)才需要去解析,如果在初始化時(shí)此方法還沒有被用到,那解析自然也完全沒有必要了

          初始化

          這一階段主要做兩件事

          1. 初始化靜態(tài)變量,為其賦值

          2. 執(zhí)行靜態(tài)代碼塊內(nèi)容

          無論是初始化靜態(tài)變量還是執(zhí)行靜態(tài)代碼塊,java 編譯器編譯后, 它們都會(huì)被一起置于一個(gè)被稱為 clinit 的方法中,并且 JVM 會(huì)對(duì)其加鎖以保證此方法只會(huì)被執(zhí)行一次,只有在初始化完成之后,類才真正成為可執(zhí)行狀態(tài),另外需要注意的,在子類的 clinit 完成之前,JVM 會(huì)確保父類的 clinit 也已經(jīng)完成了,這從繼承的角度也容易理解,子類畢竟繼承著父類,只有父類初始化可用了,子類才能放心繼承或者說使用父類的方法等。

          這里有一個(gè)需要注意的點(diǎn)是如果是 final 的靜態(tài)變量,且其類型是基本類型或字符串時(shí),該字段會(huì)被標(biāo)記為常量值,其初始化由 JVM 完成,而不會(huì)被放入 clinit,比如如下類靜態(tài)變量

          public?class?Test?{
          ????private?static?final?int?field?=?1;
          }

          這個(gè) field 由于是常量值,所以并不會(huì)放入 clinit,而是由 JVM 來完成初始化

          那么什么時(shí)候會(huì)執(zhí)行初始化呢,《Java 虛擬機(jī)規(guī)范》規(guī)定了六種情況必須立即對(duì)類進(jìn)行初始化

          1、 遇到 new、getstatic、putstatic 或 invokestatic 這四條字節(jié)碼指令的時(shí)候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化.
          生成這四條指令的最常見的java代碼場景是:

          • 使用 new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候

          • 讀取或設(shè)置一個(gè)類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候

            需要特別強(qiáng)調(diào)這一條,這里針對(duì)的是類讀取或設(shè)置本類的靜態(tài)字節(jié),如果子類讀取父類的靜態(tài)字段,父類會(huì)初始化,但子類不會(huì),比如有以下代碼

            public?class?SuperClass{
            ??static?{
            ??????System.out.println("SuperClass?init");
            ??}
            ??public?static?int?value?=?10;
            }

            public?class?SubClass?extends?SuperClass?{
            ??static?{
            ??????System.out.println("SubClass?init");
            ??}
            }

            public?class?NotInitialization?{
            ??public?static?void?main(String[]?args)?{
            ??????System.out.println(SubClass.value);
            ??}
            }

            則執(zhí)行的輸出為

            SuperClass?init
            10

            可以看到子類獲取子父類的靜態(tài)變量會(huì)讓父類初始化,但子類自身并不會(huì)被初始化

          • 調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候

          2、使用 java.lang.reflect 包的方法對(duì)類進(jìn)行反射調(diào)用的時(shí)候,如果類沒有進(jìn)行初始化,則需要先觸發(fā)其初始化

          3、當(dāng)初始化一個(gè)類的時(shí)候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化

          4、當(dāng)虛擬機(jī)啟動(dòng)的時(shí)候,用戶需要指定一個(gè)要執(zhí)行的主類(包含 main 方法的那個(gè)類),虛擬機(jī)會(huì)先初始化這個(gè)主類

          5、當(dāng)使用 jdk7 新加入的動(dòng)態(tài)語言支持的時(shí)候,如果一個(gè) java,lang.invoke.MethodHandler 實(shí)例的最后解析結(jié)果是REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial 四種類的方法句柄,并且這個(gè)方法句柄對(duì)應(yīng)的類沒有進(jìn)行過初始化,那么需要先觸發(fā)其初始化.

          6、(新)當(dāng)一個(gè)接口中定義了 JDK8 新加入的默認(rèn)方法(被default關(guān)鍵字修飾的接口方法)時(shí),如果這個(gè)接口的實(shí)現(xiàn)類發(fā)生了初始化,那么該接口要在其之前初始化

          7、 當(dāng)初次調(diào)用 MethodHandle 實(shí)例時(shí),初始化該 MethodHandle 指向的方法所在的類。

          這六種場景的行為稱為對(duì)一個(gè)類型的主動(dòng)引用.除此之外,所有的引用類型的方式都不會(huì)觸發(fā)其初始化,稱為被動(dòng)引用

          看完這些相信你能回答開頭的單例模式為啥是安全可行的

          簡單地作個(gè)總結(jié)

          怎么來更通俗地理解加載鏈接初始化這些階段呢,其實(shí)我之前常說要理解技術(shù)概念,代入生活中的場景會(huì)更容易理解,比如我們要蓋房子,你總要圖紙吧(字節(jié)碼文件),按圖紙建筑加工(加載)后成了一座房子(類對(duì)象),但此時(shí)的房子還只是毛坯房,還不能住人,如果這個(gè)房子蓋了沒人住,那之后的裝修等過程就沒必要做了,這就是為什么上文定義了Test[] list = new Test[10];?這樣的數(shù)組變量只是加載的原因,因?yàn)槟銢]有調(diào)用 Test 相關(guān)的方法等操作,后續(xù)的步驟就沒有必要做了,但如果房子蓋好了之后你要入住,那首先這是個(gè)毛坯房,總得找人驗(yàn)下房(鏈接中的驗(yàn)證)吧,不然要是出現(xiàn)一些狀況(比如把承重墻敲了成為了危房)這房子根本就不符合驗(yàn)收標(biāo)準(zhǔn)總得拒收吧,好了,驗(yàn)收通過之后那就可以開始裝修了,為沙發(fā),電視等預(yù)留好空間(準(zhǔn)備),此時(shí)你只是在相應(yīng)的地方標(biāo)記了一下,A 位置留出來給電視,B 位置留出現(xiàn)給沙發(fā),此時(shí)就相當(dāng)只是做了一個(gè)符號(hào)引用,但你真正要看電視的時(shí)候,此時(shí)沒有,那么你就得去買來裝到對(duì)應(yīng)的位置上,這就是解析,當(dāng)把房子裝修完成之后(即初始化完成),此時(shí)的房子才是可用狀態(tài)(即類處于可用狀態(tài)),才可以交付給人入住。另外不難看出,解析這一步是可以放到初始化之后的,就就好比,雖然你為電視預(yù)留了位置,但你不看不買電視也照樣能夠入駐。

          本文作者:碼海

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品久久久久久妇女6080 | 最近中文字幕免费MV第一季歌词怀孕 | 天堂网男人| 手机免费看A V | 91精品国产99久久久久久 |