<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      深入解析 JVM 類加載機制:從字節(jié)碼到運行時對象

      一、概述:為什么需要類加載?

      Java 語言的核心特性之一是"一次編寫,到處運行",這背后的關(guān)鍵在于 Java 虛擬機(JVM)和其類加載機制。當(dāng)我們編寫好 Java 代碼并將其編譯為 .class 字節(jié)碼文件后,這些靜態(tài)的字節(jié)碼需要被加載到 JVM 中才能變?yōu)榭蓤?zhí)行的動態(tài)對象。類加載就是這個轉(zhuǎn)換過程的核心環(huán)節(jié)。

      理解類加載機制能幫助我們:

      • 深入理解 Java 動態(tài)擴展機制(如 SPI、熱部署等技術(shù)原理)
      • 優(yōu)化程序性能,理解哪些階段耗時及如何調(diào)整參數(shù)優(yōu)化
      • 解決實際開發(fā)中遇到的 ClassNotFoundException、NoSuchMethodError、IllegalAccessError 等異常
      • 實現(xiàn)高級技巧,如編寫自定義類加載器實現(xiàn)模塊化、代碼加密等功能

      類加載的完整生命周期包括加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載七個階段。其中前五個階段是類加載的核心過程,下面我們將詳細(xì)解析每個階段。

      二、加載 (Loading) - "采購與入庫"階段

      核心思想

      加載階段是類加載過程的第一步,它的核心任務(wù)非常明確:找到類的字節(jié)碼,并以 JVM 內(nèi)部規(guī)定的格式把它存起來,同時創(chuàng)建一個訪問入口。

      將這個階段比喻為一家公司的采購和入庫部門非常貼切:

      加載階段步驟 公司比喻 技術(shù)對應(yīng)
      1. 獲取二進制流 采購部門尋找貨源 從JAR、網(wǎng)絡(luò)、動態(tài)生成等處獲取字節(jié)碼
      2. 轉(zhuǎn)化存儲結(jié)構(gòu) 入庫部門按標(biāo)準(zhǔn)存放 將字節(jié)流轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
      3. 生成Class對象 創(chuàng)建庫存查詢目錄 在Java堆中創(chuàng)建 java.lang.Class 對象

      詳細(xì)過程

      1. "采購" - 通過類名獲取二進制字節(jié)流

      任務(wù):根據(jù)類的全限定名(如 java.lang.String),去找到并拿到這個類的"原始產(chǎn)品"——二進制字節(jié)流(符合 Class 文件格式的二進制數(shù)據(jù))。

      關(guān)鍵點:《Java虛擬機規(guī)范》只規(guī)定了要拿到什么,但沒規(guī)定從哪里拿、怎么拿。這個開放性設(shè)計是 Java 強大擴展能力的基石。

      "采購"渠道的多樣性

      • 從本地倉庫拿:從 ZIP、JAR、EAR、WAR 等壓縮包中讀取(最常見的方式)
      • 從網(wǎng)絡(luò)上訂貨:從網(wǎng)絡(luò)上下載(如早期的 Web Applet
      • 自己生產(chǎn)(OEM):在運行時動態(tài)計算生成(動態(tài)代理技術(shù)是典型例子)
      • 由其他原材料加工:由 JSP 文件生成對應(yīng)的 Class 文件
      • 從加密倉庫取:從加密的文件中讀取,讀取時再實時解密(常見代碼保護手段)
      • 從數(shù)據(jù)庫里讀:特定中間件服務(wù)器(如 SAP Netweaver)會把程序代碼存到數(shù)據(jù)庫里
      // 自定義類加載器示例:從特定路徑加載類
      public class CustomClassLoader extends ClassLoader {
          private String classPath;
          
          public CustomClassLoader(String classPath) {
              this.classPath = classPath;
          }
          
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              // 1. "采購":根據(jù)類名找到文件,并讀取為字節(jié)數(shù)組 byte[]
              byte[] classData = loadClassData(name);
              if (classData == null) {
                  throw new ClassNotFoundException();
              }
              // 2. "質(zhì)檢與入庫":調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)換為Class對象
              //    此方法會完成驗證、準(zhǔn)備等后續(xù)步驟
              return defineClass(name, classData, 0, classData.length);
          }
          
          private byte[] loadClassData(String className) {
              // 實現(xiàn)從特定路徑(如加密文件)讀取類文件的邏輯
              // 將類名轉(zhuǎn)換為文件路徑
              String path = classNameToPath(className);
              try {
                  InputStream is = new FileInputStream(path);
                  ByteArrayOutputStream baos = new ByteArrayOutputStream();
                  int bufferSize = 4096;
                  byte[] buffer = new byte[bufferSize];
                  int bytesNumRead;
                  while ((bytesNumRead = is.read(buffer)) != -1) {
                      baos.write(buffer, 0, bytesNumRead);
                  }
                  return baos.toByteArray();
              } catch (IOException e) {
                  return null;
              }
          }
          
          private String classNameToPath(String className) {
              // 將類名轉(zhuǎn)換為文件路徑
              return classPath + File.separatorChar + 
                     className.replace('.', File.separatorChar) + ".class";
          }
      }
      

      2. "質(zhì)檢與入庫" - 轉(zhuǎn)化存儲結(jié)構(gòu)

      任務(wù):把上一步拿到的"原始產(chǎn)品"(字節(jié)流),轉(zhuǎn)換成 JVM 方法區(qū)這個"中央倉庫"所能識別的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

      比喻:采購回來的貨物可能有各種包裝(不同來源的字節(jié)流),入庫部門需要把它們拆包,按照公司倉庫(方法區(qū))自己的貨架標(biāo)準(zhǔn)和分類方式重新擺放好。

      注意:方法區(qū)內(nèi)部的具體數(shù)據(jù)結(jié)構(gòu)完全由各個 JVM 實現(xiàn)自行決定,《規(guī)范》不做要求。就像不同公司的倉庫管理系統(tǒng)可以完全不同。

      3. "創(chuàng)建庫存目錄" - 生成 Class 對象

      任務(wù):在 Java 堆 中創(chuàng)建一個 java.lang.Class 對象。

      作用:這個對象就像倉庫的總目錄訪問接口。程序想要訪問方法區(qū)中關(guān)于這個類的所有元數(shù)據(jù)信息(比如有哪些方法、哪些字段),都必須通過這個 Class 對象來進行。

      比喻:貨物已經(jīng)按規(guī)則存入了大型立體倉庫(方法區(qū)),為了方便大家查找,我們在辦公室的電腦(Java 堆)里建了一個數(shù)據(jù)庫條目(Class 對象),通過它就能查到貨物在哪、有多少。

      兩種特殊的"貨物":數(shù)組 vs 非數(shù)組類

      非數(shù)組類

      加載方式:就是我們上面說的"采購入庫"流程。開發(fā)人員可以高度控制這個過程,通過自定義類加載器(重寫 findClass() 方法)來決定如何獲取字節(jié)流,從而實現(xiàn)熱部署、模塊化等高級特性。

      數(shù)組類

      加載方式:數(shù)組類比較特殊,它不是通過類加載器"采購"回來的,而是由 JVM 直接在生產(chǎn)線上"組裝"出來的。

      規(guī)則

      1. 如果數(shù)組的元素類型是引用類型(比如 String[], Object[]),那么 JVM 會先去加載這個元素類型。這個數(shù)組類會被標(biāo)記為與加載該元素類型的類加載器關(guān)聯(lián)。
      2. 如果數(shù)組的元素類型是基本類型(比如 int[], boolean[]),那么 JVM 會直接把數(shù)組類標(biāo)記為與啟動類加載器關(guān)聯(lián)。

      訪問性:數(shù)組的訪問權(quán)限和它的元素類型一致。int[] 的訪問性是 public。

      重要細(xì)節(jié)

      • 交叉進行:加載階段和連接階段(尤其是驗證)并不是完全割裂的。為了性能,JVM 可能在拿到一部分字節(jié)流后就開始進行文件格式驗證(比如檢查魔數(shù)),但主體上仍然保持"先加載,后連接"的順序
      • 可控性:在"加載"階段的"獲取二進制字節(jié)流"這個動作上,開發(fā)人員擁有最大的控制權(quán),這是通過自定義類加載器實現(xiàn)的。

      簡單來說,"加載"階段就是 JVM 的物資準(zhǔn)備階段,它為后續(xù)的校驗、初始化等步驟準(zhǔn)備好了最重要的原材料——類的二進制數(shù)據(jù),并建立了訪問這些數(shù)據(jù)的基礎(chǔ)設(shè)施。它的開放性設(shè)計為 Java 的繁榮生態(tài)奠定了堅實的基礎(chǔ)。

      三、驗證 (Verification) - "嚴(yán)格安檢"階段

      核心思想

      驗證階段就像是 JVM 的超級嚴(yán)格的安全檢查站。它的唯一目的就是:確保你要加載的 Class 文件是個"良民",而不是一個攜帶病毒或邏輯炸彈的"黑客",從而保證 JVM 自身的安全。

      因為 Class 文件不一定來自 Java 編譯器,它可能被篡改或惡意生成,所以 JVM 絕不能信任任何外部傳來的字節(jié)流,必須進行徹底檢查。

      安全檢查的四個關(guān)卡(四大驗證)

      驗證過程非常復(fù)雜,但總體上會按順序通過四個關(guān)卡的檢查。只有全部通過,Class 才能被成功加載。

      1. 文件格式驗證 - "文件格式與合法性檢查"

      檢查什么? 檢查字節(jié)流是否符合 Class 文件格式規(guī)范。這是基于二進制字節(jié)流本身的檢查。

      比喻:就像海關(guān)檢查護照。官員會看:

      • 護照的封面對不對?(魔數(shù)是不是 0xCAFEBABE?)
      • 護照的版本是否有效?(主次版本號是否支持?)
      • 護照里的欄目填寫是否正確無誤?(常量池里的常量類型、索引值是否合法?UTF-8編碼是否正確?)

      目的:保證這個字節(jié)流能被正確解析并存入方法區(qū)。只有通過此關(guān)檢查,數(shù)據(jù)才會被存入方法區(qū),后續(xù)檢查都將基于方法區(qū)內(nèi)的結(jié)構(gòu)進行,而不再直接操作字節(jié)流。

      2. 元數(shù)據(jù)驗證 - "語義檢查"

      檢查什么? 對類的元信息進行語義分析,看是否符合 Java 語言規(guī)范。

      比喻:就像公司HR進行背景調(diào)查。HR會核實:

      • 你的簡歷上寫了你爸的名字(父類),這是真的嗎?(除了Object,所有類都應(yīng)有父類)
      • 你爸是最高法院的終身大法官(final 類)嗎?如果是,那就不能聲稱繼承了他。
      • 你聲稱掌握所有必要技能(非抽象類),那你是否真的實現(xiàn)了你爸或你接口要求的所有方法?
      • 你的經(jīng)歷描述有沒有和你爸的簡歷沖突?(比如重寫了父類的 final 方法,或者方法重載不符合規(guī)則

      目的:保證類的元數(shù)據(jù)信息沒有語義上的矛盾。

      3. 字節(jié)碼驗證 - "邏輯檢查"(最復(fù)雜的一關(guān))

      檢查什么?方法體中的代碼進行邏輯校驗。這是最復(fù)雜、最耗時的部分。

      比喻:就像電影審查部門審核劇本。審查員要確保劇本邏輯通順,不會導(dǎo)致演員(JVM)在表演時出事故:

      • 演員的道具使用是否合理?會不會出現(xiàn)"拿起一把手槍(int類型),卻當(dāng)火箭筒(long類型)來用"這種類型錯誤?
      • 劇本里的跳轉(zhuǎn)指令(如 goto)會不會讓演員跳下舞臺(跳到方法體之外)?
      • 類型轉(zhuǎn)換是否合理?能讓一個普通人(子類)扮演超人(父類),但不能讓超人(父類)強行扮演一個具體的普通人(子類),更不能讓一棵樹(無關(guān)類)來扮演超人。

      著名的"停機問題":理論上,無法通過程序100%準(zhǔn)確地判斷另一段程序是否包含所有類型的邏輯錯誤(就像無法通過算法判斷任何程序是否會無限循環(huán)下去)。所以,通過字節(jié)碼驗證的程序未必絕對安全,但沒通過的一定有問題!

      性能優(yōu)化 - StackMapTable:為了加速這個耗時的過程,JDK 6 之后,編譯器(javac)會在編譯時預(yù)先計算好很多驗證信息(記錄每個關(guān)鍵點的變量類型和操作數(shù)棧狀態(tài)),并保存在 Class 文件的 StackMapTable 屬性中。JVM 驗證時只需要對照檢查這些預(yù)先生成的記錄即可,大大提高了效率。

      public class VerificationExample {
          public void method(boolean flag) {
              // 字節(jié)碼驗證會分析出,無論走哪個分支,操作數(shù)棧在方法返回前都是平衡的
              if (flag) {
                  System.out.println("True");
              } else {
                  System.out.println("False");
              }
              // 如果這里缺少 return 語句,字節(jié)碼驗證會失敗
              // 編譯器會報錯:Missing return statement
          }
          
          // 可能引起驗證問題的示例
          public void problematicMethod() {
              // 理論上,這里可能包含驗證器無法通過的復(fù)雜控制流
              // 但現(xiàn)代編譯器通常會在編譯期就阻止這樣的代碼
          }
      }
      

      4. 符號引用驗證 - "外部依賴檢查"

      檢查什么? 發(fā)生在解析階段(將符號引用轉(zhuǎn)換為直接引用時)。檢查類是否能夠成功訪問到它所引用的外部類、方法、字段等資源。

      比喻:就像項目啟動前的最終資源確認(rèn)。你要開始一個項目,需要確認(rèn):

      • 你依賴的其他公司(外部類) 真的存在嗎?
      • 那家公司的某個部門(方法/字段) 真的存在嗎?
      • 你有權(quán)限訪問那個部門的資源嗎?(訪問權(quán)限檢查,比如不能訪問別人的 private 方法)

      目的:確保解析動作能夠正常執(zhí)行。如果失敗,會拋出 NoSuchFieldError、NoSuchMethodError、IllegalAccessError 等異常。

      總結(jié)與要點

      驗證階段 核心問題 比喻 失敗后果
      1. 文件格式驗證 "這是一個合法的Class文件嗎?" 海關(guān)檢查護照 拋出 VerifyError
      2. 元數(shù)據(jù)驗證 "這個類的描述信息自洽嗎?" HR背景調(diào)查 拋出 VerifyError
      3. 字節(jié)碼驗證 "這個類的方法邏輯正確嗎?" 劇本審查 拋出 VerifyError
      4. 符號引用驗證 "這個類能訪問到它需要的所有資源嗎?" 項目資源確認(rèn) 拋出 IncompatibleClassChangeError

      重要提示

      1. 非常耗時:驗證階段是類加載過程中工作量最大、最耗性能的部分之一。
      2. 可以關(guān)閉:如果確認(rèn)所有代碼都是可靠且反復(fù)驗證過的(例如生產(chǎn)環(huán)境),可以使用 -Xverify:none 參數(shù)來關(guān)閉大部分驗證措施,以顯著提高類加載速度。但這會帶來安全風(fēng)險。
      3. 設(shè)計演進:驗證規(guī)則在《Java虛擬機規(guī)范》中變得越來越具體和復(fù)雜,體現(xiàn)了對安全性日益增長的要求。

      總而言之,驗證階段是 JVM 抵御惡意代碼的第一道也是最重要的一道防線,它通過層層遞進的嚴(yán)格檢查,確保了后續(xù)操作的基礎(chǔ)安全。

      四、準(zhǔn)備 (Preparation) - "賦默認(rèn)值"階段

      核心思想

      準(zhǔn)備階段是 JVM 為類變量(static 變量) "分配房間并給每個房間貼上默認(rèn)值標(biāo)簽"的階段。此時尚未執(zhí)行任何Java代碼,所以程序員指定的值還不會被賦予。

      一個比喻:布置新房

      想象一下,JVM 正在為一個新來的"類"布置它的靜態(tài)區(qū)域(方法區(qū))。

      1. 加載階段:已經(jīng)確定了這個"類"需要多大的靜態(tài)空間(有多少個 static 變量)。
      2. 準(zhǔn)備階段:JVM 開始分配這些空間,并在每個空間里放上一個默認(rèn)的初始值。
      3. 初始化階段:后面才會執(zhí)行程序員編寫的 static 代碼塊或賦值語句,把這些默認(rèn)值替換成程序員真正想要的值。

      兩個關(guān)鍵要點

      1. 分配誰?不分配誰?

      • 僅分配類變量 (Class Variables):即被 static 修飾的變量。準(zhǔn)備階段只處理它們。
      • 不分配實例變量 (Instance Variables):沒有被 static 修飾的變量。它們要等到將來創(chuàng)建對象實例時,才會隨著對象一起在 Java 堆中分配內(nèi)存和初始化。

      比喻:這就像給一個公司布置辦公室。

      • static 變量是公司的公共財產(chǎn)(如前臺電話、會議室投影儀)。公司一注冊成立(類被加載),這些就要準(zhǔn)備好。
      • 實例變量是員工的個人辦公用品(如員工的電腦、筆記本)。要等員工入職(對象被 new 出來)才會分配。

      2. "初始值"是什么?零值!

      在準(zhǔn)備階段,JVM 會給類變量賦予一個系統(tǒng)默認(rèn)的"零值",而不是程序員在代碼中寫的值。

      為什么? 因為此時還沒有開始執(zhí)行任何 Java 方法(包括 <clinit> 類構(gòu)造器),賦值語句自然也不會執(zhí)行。

      例子與對比

      Java 代碼 準(zhǔn)備階段后的值 原因
      public static int value = 123; 0 (int的零值) 賦值 123putstatic 指令在 <clinit> 方法中,尚未執(zhí)行。
      public static boolean enabled; false (boolean的零值) 尚未被顯式初始化。
      public static Object obj; null (reference的零值) 尚未被顯式初始化。

      基本數(shù)據(jù)類型的零值表

      數(shù)據(jù)類型 零值
      int, byte, short, char 0
      long 0L
      float 0.0f
      double 0.0d
      boolean false
      reference (引用類型) null

      特殊情況:常量 (static final)

      有一種特殊情況,它打破了"準(zhǔn)備階段總是賦零值"的規(guī)則。

      規(guī)則:如果類字段的字段屬性表中存在 ConstantValue 屬性,那么在準(zhǔn)備階段,變量值就會直接被初始化為 ConstantValue 屬性所指定的值,而不是零值。

      何時生成 ConstantValue 屬性?
      當(dāng)變量同時被 staticfinal 修飾,并且它的值是編譯期常量時,編譯器 (javac) 會為它生成 ConstantValue 屬性。

      例子

      public static final int CONST_VALUE = 123; // 編譯期常量
      

      對于這行代碼,在準(zhǔn)備階段結(jié)束后,CONST_VALUE 的值就是 123,而不是 0。

      為什么?
      因為 123 是一個編譯期就能確定的常量,JVM 認(rèn)為沒有必要先賦零值,再在初始化階段改為 123。直接在準(zhǔn)備階段一步到位,更高效。

      public class PreparationExample {
          public static int normalStatic = 123;      // 準(zhǔn)備階段后值為 0
          public static final int CONST_STATIC = 456;// 準(zhǔn)備階段后值為 456
          
          // 非常量final字段,準(zhǔn)備階段后仍為0
          public static final int NON_CONST_STATIC;
          static {
              NON_CONST_STATIC = 789; // 在初始化階段才賦值
          }
      }
      

      內(nèi)存位置的演變

      • 邏輯概念:類變量在方法區(qū)分配。
      • 物理實現(xiàn)
        • 在 JDK 7 及之前,HotSpot 使用永久代來實現(xiàn)方法區(qū),類變量確實在永久代。
        • 在 JDK 8 及之后,永久代被移除,類變量隨著 Class 對象一起存放在 Java 堆 中。
        • 但"方法區(qū)"這個邏輯概念依然存在,所以我們從邏輯上仍然說類變量屬于方法區(qū)。

      總結(jié)

      準(zhǔn)備階段是一個承上啟下的簡單階段,它只做兩件事:

      1. 分配內(nèi)存:為 static 變量在方法區(qū)(邏輯上)分配空間。
      2. 賦系統(tǒng)初始值:為這些變量賦上對應(yīng)數(shù)據(jù)類型的零值 (0, false, null 等)。

      記住那個例外:被 static final 修飾的編譯期常量,會在準(zhǔn)備階段直接賦值為代碼中寫的值。

      這個過程完成后,類變量就都有了"默認(rèn)值",等待著在初始化階段被程序員寫的代碼賦予"真正的值"。

      五、解析 (Resolution) - "查地址"階段

      核心思想

      解析階段是 JVM 的 "查地址" 階段。它的任務(wù)非常明確:將常量池中的符號引用(一個名字)替換為直接引用(一個具體的地址或句柄)。

      核心概念:符號引用 vs. 直接引用

      理解這兩個概念是理解解析階段的關(guān)鍵。

      特性 符號引用 (Symbolic Reference) 直接引用 (Direct Reference)
      是什么 一個名字、一個描述 一個指針、一個偏移量、一個句柄
      內(nèi)容 用文本形式描述目標(biāo)(如 java/lang/Object 直接指向目標(biāo)在內(nèi)存中的位置
      例子 像通訊錄里的 "張三" 像張三的 "手機號碼""家庭住址"
      與內(nèi)存的關(guān)系 無關(guān)。它只是一個字符串,不關(guān)心目標(biāo)是否已加載到內(nèi)存。 緊密相關(guān)。直接指向內(nèi)存中的具體位置,目標(biāo)必須已存在。
      特點 統(tǒng)一:所有JVM實現(xiàn)的Class文件中的符號引用格式都是一樣的。 不統(tǒng)一:不同JVM實現(xiàn)的內(nèi)存布局不同,翻譯出的直接引用也不同。

      簡單比喻

      • 編譯時:你的代碼里寫 user.getName()。編譯器只知道你要調(diào)用一個叫 getName 的方法,它把這個方法名(符號引用)寫在Class文件的常量池里。
      • 解析時:JVM 在加載類后,需要真正執(zhí)行 user.getName() 了。這時,它就去常量池找到 getName 這個名字(符號引用),然后查表,找到這個方法在內(nèi)存中的實際入口地址(直接引用),并將常量池中的記錄替換成這個地址。以后每次調(diào)用,就直接使用這個地址,不再需要查找。

      解析的時機

      《Java虛擬機規(guī)范》沒有嚴(yán)格規(guī)定解析發(fā)生的確切時間,只要求在執(zhí)行某些特定字節(jié)碼指令(如 getfield, invokevirtual, new 等)之前,必須先對它們用到的符號引用進行解析

      因此,JVM 有兩種策略:

      1. eager resolution (急切解析):在類加載完成后,立刻解析所有符號引用。
      2. lazy resolution (懶惰解析):等到第一次使用某個符號引用時,才去解析它。

      現(xiàn)在的主流JVM(如HotSpot)默認(rèn)使用懶惰解析,這可以提升性能,避免加載一個類時就去解析它所有可能還不會用到的其他類。

      解析的內(nèi)容(四大類)

      解析動作主要針對類或接口、字段、類方法、接口方法等符號引用進行。其核心邏輯可以概括為:先解析所有者,再在其基礎(chǔ)上查找目標(biāo)成員。

      1. 類或接口的解析 (從 CONSTANT_Class_info 解析)

      目標(biāo):將類似 java/lang/Object 這樣的符號引用,解析為JVM內(nèi)部表示該類的數(shù)據(jù)結(jié)構(gòu)(如Klass)的直接引用。

      步驟

      1. 加載:如果符號引用代表的是一個普通類(非數(shù)組),JVM 會將這個全限定名交給當(dāng)前類的類加載器去加載這個類。這個過程會觸發(fā)該類自身的加載、驗證、準(zhǔn)備等階段。
      2. 權(quán)限檢查:檢查當(dāng)前類 D 是否有權(quán)訪問這個被解析的類 C。如果沒有(例如,C 不是 public 且也不和 D 在同一個包內(nèi)),則拋出 IllegalAccessError。
      // 類解析示例
      public class ClassResolutionExample {
          public void createObject() {
              // 這里會觸發(fā)對java.util.ArrayList類的解析
              // 1. 檢查常量池中的符號引用"java/util/ArrayList"
              // 2. 使用當(dāng)前類加載器加載ArrayList類(如果尚未加載)
              // 3. 檢查訪問權(quán)限
              // 4. 將符號引用替換為直接引用
              java.util.ArrayList list = new java.util.ArrayList();
          }
      }
      

      2. 字段解析 (從 CONSTANT_Fieldref_info 解析)

      目標(biāo):解析一個字段,例如 java/lang/System.out。

      步驟

      1. 解析所有者:先解析字段所屬的類或接口的符號引用(即先完成上一步的類解析)。
      2. 字段查找:在成功解析出的類或接口 C 中,按以下順序自下而上地查找匹配的字段:
        • 步驟1:在 C 自身中查找。
        • 步驟2:如果 C 實現(xiàn)了接口,會從上至下遞歸搜索它的所有接口。
        • 步驟3:如果 C 不是 Object,則自下而上地遞歸搜索它的父類。
      3. 如果找到,返回字段的直接引用;如果找不到,拋出 NoSuchFieldError。
      4. 權(quán)限檢查:檢查當(dāng)前類是否有權(quán)訪問該字段(如不能訪問 private 字段),否則拋出 IllegalAccessError

      3. 方法解析 (從 CONSTANT_Methodref_info 解析)

      目標(biāo):解析一個的方法(非接口方法)。

      步驟

      1. 解析所有者 & 合法性檢查:解析方法所屬的類 C。如果發(fā)現(xiàn) C 是一個接口,直接拋出 IncompatibleClassChangeError(因為 invokevirtual 指令不能調(diào)用接口方法)。
      2. 方法查找:在類 C 中查找:
        • 步驟1:在 C 自身中查找。
        • 步驟2:在 C 的父類中遞歸查找。
        • 步驟3:在 C 實現(xiàn)的接口列表中查找(這一步不會找到具體方法,只會用于錯誤檢查)。如果在這里找到,說明 C 是一個抽象類但沒有實現(xiàn)接口的方法,拋出 AbstractMethodError
      3. 找到則返回直接引用,否則拋出 NoSuchMethodError。
      4. 權(quán)限檢查:檢查訪問權(quán)限,失敗則拋出 IllegalAccessError。
      // 方法解析示例
      public class MethodResolutionExample {
          public void callMethod() {
              // 這里會觸發(fā)對toString()方法的解析
              // 1. 解析當(dāng)前類 -> Object類
              // 2. 在Object類中查找toString方法
              // 3. 檢查訪問權(quán)限(public方法,可訪問)
              // 4. 將符號引用替換為直接引用
              String str = toString();
          }
      }
      

      4. 接口方法解析 (從 CONSTANT_InterfaceMethodref_info 解析)

      目標(biāo):解析一個接口的方法。

      步驟

      1. 解析所有者 & 合法性檢查:解析方法所屬的接口 C。如果發(fā)現(xiàn) C 是一個,直接拋出 IncompatibleClassChangeError。
      2. 方法查找
        • 步驟1:在接口 C 自身中查找。
        • 步驟2:在接口 C 的父接口中遞歸查找,直到 Object 類。
      3. 找到則返回直接引用,否則拋出 NoSuchMethodError
      4. 權(quán)限檢查:在 JDK 9 之前,接口方法都是 public,無需檢查。JDK 9 引入私有靜態(tài)方法后,也需要進行權(quán)限檢查。

      緩存

      為了提升性能,除 invokedynamic 指令外,解析結(jié)果會被緩存。一旦一個符號引用被成功解析,下次再遇到它時就會直接使用緩存的直接引用,避免重復(fù)解析。

      特殊的 invokedynamic 指令

      invokedynamic 是為動態(tài)語言(如 JavaScript)支持而設(shè)計的,它的解析邏輯是"一次解析,僅一次有效"。它的解析結(jié)果不會被緩存供其他 invokedynamic 指令使用,因為每次調(diào)用都可能是動態(tài)的、不同的。

      總結(jié)

      解析階段是連接符號世界和現(xiàn)實世界的橋梁。它通過一系列精心設(shè)計的步驟,將Class文件中的文本名字(符號引用)轉(zhuǎn)換為JVM內(nèi)存中的具體地址(直接引用),同時確保了Java語言的安全性(權(quán)限檢查)和一致性(繼承規(guī)則)。這個過程是Java實現(xiàn)動態(tài)擴展多態(tài)特性的底層基石。

      六、初始化 (Initialization) - "執(zhí)行構(gòu)造代碼"階段

      核心思想

      初始化階段是類加載的最后一步,也是真正開始執(zhí)行程序員編寫的 Java 代碼的一步。在這一步,JVM 會將靜態(tài)變量和靜態(tài)代碼塊中你寫的邏輯付諸實施。

      你可以把它想象成一個設(shè)備的最終啟動和自檢程序。之前加載、驗證、準(zhǔn)備階段只是把設(shè)備(類)運進工廠、拆箱、檢查零件、裝上貨架(賦零值)。而現(xiàn)在,要按下電源開關(guān),執(zhí)行制造商(程序員)設(shè)定的啟動指令了。

      主角:<clinit>() 方法

      初始化階段就是執(zhí)行一個叫做 <clinit>() 方法的過程。這個方法不是程序員手寫的,而是由 javac 編譯器自動生成的。

      • <clinit> 代表 class initialization
      • 它是由編譯器自動收集類中的所有靜態(tài)變量的賦值語句靜態(tài)代碼塊 (static {} 塊) 中的語句合并而成的。
      • 收集的順序就是這些語句在源文件中出現(xiàn)的順序。

      舉個例子

      public class Test {
          static int i = 1;       // 賦值語句1
          static {                // 靜態(tài)代碼塊
              i = 2;
              j = 3; 
              // System.out.println(j); // 這里如果訪問j,就是非法前向引用!
          }
          static int j = 4;       // 另一個賦值語句2
      
          // 編譯器生成的 <clinit>() 方法邏輯順序:
          // i = 1;
          // i = 2;
          // j = 3;
          // j = 4;
      }
      // 最終 i=2, j=4
      

      關(guān)鍵特性與規(guī)則

      1. 順序重要性與"非法前向引用"

      • 編譯器收集語句的順序就是源碼中的順序。
      • 靜態(tài)代碼塊中只能訪問定義在它之前的靜態(tài)變量。
      • 對于定義在它之后的變量,靜態(tài)代碼塊可以為其賦值,但不能訪問其值(讀?。?。如果嘗試訪問,編譯器會報"非法前向引用"錯誤。
      • 為什么? 因為雖然 j 的內(nèi)存空間在準(zhǔn)備階段已經(jīng)分配好(初始值為0),但在 <clinit>() 方法中,j = 4 的賦值操作還沒執(zhí)行。如果你在之前的靜態(tài)塊中讀取它,邏輯上是混亂的。

      2. 父類優(yōu)先原則

      • JVM 會保證在子類的 <clinit>() 方法執(zhí)行前,其父類的 <clinit>() 方法已經(jīng)執(zhí)行完畢
      • 這意味著父類的靜態(tài)代碼塊和靜態(tài)變量賦值會先于子類的執(zhí)行。
      • 因此,整個 JVM 中第一個被執(zhí)行 <clinit>() 方法的類肯定是 java.lang.Object。

      例子

      class Parent {
          public static int A = 1; // 1. 先執(zhí)行這個賦值
          static {
              A = 2;               // 2. 再執(zhí)行這個,A 最終為 2
          }
      }
      class Sub extends Parent {
          public static int B = A; // 3. 最后執(zhí)行這個,B 的值是父類 A 的最終值 2
      }
      // 輸出 Sub.B 的結(jié)果是 2
      

      3. 不是必需的

      • 如果一個類中沒有靜態(tài)代碼塊,也沒有對靜態(tài)變量的顯式賦值操作(比如只有 static int i;),那么編譯器可以不為這個類生成 <clinit>() 方法。

      4. 接口的初始化

      • 接口也有 <clinit>() 方法(因為接口可以有靜態(tài)變量,JDK8后還可以有靜態(tài)方法)。
      • 關(guān)鍵區(qū)別:執(zhí)行一個接口的 <clinit>() 方法并不需要先執(zhí)行其父接口的 <clinit>()。父接口只有在真正被使用時(如其定義的變量被訪問)才會被初始化。
      • 一個類在初始化時,不會自動先去執(zhí)行它實現(xiàn)的接口的 <clinit>() 方法。

      5. 線程安全與同步(極其重要?。?/h4>
      • JVM 會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖同步。
      • 這意味著:多個線程如果同時去初始化同一個類,只有一個線程會去執(zhí)行 <clinit>() 方法,其他所有線程都會被阻塞等待。
      • 直到那個活動線程執(zhí)行完 <clinit>() 方法后,其他線程才會被喚醒,并且不會再重新執(zhí)行初始化過程。

      這個機制會導(dǎo)致一個嚴(yán)重的風(fēng)險
      如果你的 <clinit>() 方法中包含一個耗時極長的操作(比如一個死循環(huán)),或者由于某些原因卡住了,那么所有其他試圖初始化這個類的線程都會被無限期地阻塞在那里,從而導(dǎo)致系統(tǒng)癱瘓。

      static class DeadLoopClass {
          static {
              if (true) { // 為了騙過編譯器的靜態(tài)檢查
                  System.out.println("線程" + Thread.currentThread() + "開始初始化...");
                  while (true) {} // 死循環(huán)!
              }
          }
      }
      // 如果兩個線程同時嘗試初始化 DeadLoopClass,一個會進去死循環(huán),另一個會永遠阻塞等待。
      

      初始化觸發(fā)時機("主動引用")

      只有當(dāng)類被"主動引用"時,才會觸發(fā)初始化:

      1. 遇到 new, getstatic, putstatic, invokestatic 字節(jié)碼指令時。
      2. 使用 java.lang.reflect 包的方法對類進行反射調(diào)用時。
      3. 初始化一個類時,如果其父類還未初始化,則先觸發(fā)父類的初始化。
      4. 虛擬機啟動時,需指定一個包含 main() 方法的主類,虛擬機會先初始化這個主類。
      5. 使用 JDK 7 的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且這個句柄所對應(yīng)的類沒有進行過初始化。

      總結(jié)與比喻

      概念 比喻
      準(zhǔn)備階段 給新房間配好家具并貼上"空"的標(biāo)簽(賦零值)。
      初始化階段 按照主人的吩咐布置房間:把書放進書柜(i = 1),把畫掛上墻(靜態(tài)塊中的操作)。執(zhí)行 <clinit>() 方法。
      <clinit>() 方法 房間的布置清單,由管家(編譯器)根據(jù)主人的吩咐(源碼)自動生成。
      父類優(yōu)先 布置豪宅前,必須先把它所在的整個莊園都布置好
      非法前向引用 清單上要求"把花瓶放在第5號桌子上",但此時第5號桌子還沒運到房間裡(變量還未賦值),所以你無法描述它看起來怎么樣(無法訪問其值)。
      線程安全 房間一次只允許一個管家進去布置,其他管家必須在門口排隊等候,等他布置完后,大家就都知道房間已經(jīng)準(zhǔn)備好了,無需再進去。

      初始化階段是類加載過程中開發(fā)人員最能直接施加影響的階段,你寫的靜態(tài)賦值和靜態(tài)代碼塊就在這里執(zhí)行。理解它的順序規(guī)則和線程安全特性,對于編寫正確、高效的多線程程序至關(guān)重要。要特別小心在靜態(tài)初始化塊中編寫可能引起阻塞或死鎖的代碼。

      七、總結(jié)

      JVM 的類加載過程是一個嚴(yán)謹(jǐn)而精妙的系統(tǒng),它將靜態(tài)的字節(jié)碼文件轉(zhuǎn)變?yōu)檫\行時動態(tài)的 Java 對象。五個階段環(huán)環(huán)相扣:

      1. 加載是"找數(shù)據(jù)",通過靈活的類加載器獲取字節(jié)流。
      2. 驗證是"保安全",構(gòu)筑堅固的安全防線。
      3. 準(zhǔn)備是"建空間并清零",為類變量分配空間并賦零值。
      4. 解析是"查地址",將符號引用轉(zhuǎn)換為直接引用。
      5. 初始化是"賦真值",執(zhí)行靜態(tài)代碼和賦值,完成類的構(gòu)造。

      理解這個過程,不僅能讓我們更深入地理解 Java 程序的運行原理,更能為我們在實踐中解決復(fù)雜問題、進行性能優(yōu)化和實現(xiàn)高級特性提供堅實的理論基礎(chǔ)。

      posted @ 2025-09-19 08:47  佛祖讓我來巡山  閱讀(168)  評論(1)    收藏  舉報

      佛祖讓我來巡山博客站 - 創(chuàng)建于 2018-08-15

      開發(fā)工程師個人站,內(nèi)容主要是網(wǎng)站開發(fā)方面的技術(shù)文章,大部分來自學(xué)習(xí)或工作,部分來源于網(wǎng)絡(luò),希望對大家有所幫助。

      Bootstrap中文網(wǎng)

      主站蜘蛛池模板: 国产精品自拍视频第一页| 久久人与动人物a级毛片| 亚洲精品成人片在线观看精品字幕| 国产精品成人中文字幕| 国产久免费热视频在线观看| 国产一区精品在线免费看| 国产手机在线αⅴ片无码观看| 99精品日本二区留学生| 中文字幕在线精品国产| www插插插无码免费视频网站| 1000部精品久久久久久久久| 少妇又爽又刺激视频| 日本无翼乌邪恶大全彩h| 亚洲天堂一区二区成人在线| 国产午夜福利视频在线| 最新中文字幕国产精品| 精品熟女少妇免费久久| 性少妇tubevⅰdeos高清| 二区中文字幕在线观看| 天堂在线精品亚洲综合网| 亚洲嫩模一区二区三区| 国产成人综合在线观看不卡| 在线免费成人亚洲av| 久久人人妻人人做人人爽| 国产在线精品一区二区三区不卡 | 清新县| 亚洲一区av在线观看| 欧美人妻在线一区二区| 久久精品人人做人人爽电影蜜月| 亚洲综合久久精品国产高清| 亚洲AV无码午夜嘿嘿嘿| 国产精品成人观看视频国产奇米| 免费人成在线视频无码| 成人无码午夜在线观看| 浓毛老太交欧美老妇热爱乱| 国产一区二区三区亚洲精品| 欧美黑人大战白嫩在线| 少妇人妻系列无码专区视频| 国产精品人妻熟女男人的天堂 | 欧美成年黄网站色视频| 两个人的视频www免费|