Java有根兒:Class文件以及類載入器

来源:https://www.cnblogs.com/Evsward/archive/2022/05/29/jvm_class_classloader.html
-Advertisement-
Play Games

JVM 是Java的基石,Java從業者需要瞭解。JVM不是一個新的知識,網上文章很多,本篇的不同之處在於參考一手資料、內容經過反覆推敲。本文將會有篩選地研究JVM的精華部分,至少達到準系統架構師夠用的程度。本篇主要分享學習Java Class文件以及類載入器CLassLoader的知識。 ...


JVM 是Java的基石,Java從業者需要瞭解。然而相比JavaSE來講,不瞭解JVM的一般來說也不會影響到工作,但是對於有調優需求或者系統架構師的崗位來說,JVM非常重要。JVM不是一個新的知識,網上文章很多,本篇的不同之處在於參考一手資料、內容經過反覆推敲、思維邏輯更加連貫、知識更加系統化、研究路線採取按圖索驥的方式。本文將會有篩選地研究JVM的精華部分,至少達到準系統架構師夠用的程度。本篇主要分享學習Java Class文件以及類載入器CLassLoader的知識。以下是一些說明:

①由於篇幅有限,預設一些基礎背景知識已經達成了共識,不會贅述。

②本文重點研究JVM的抽象標準(或者理解為一套介面),至於實現的內容不是本文的重點學習對象。

(那麼實現的內容包括哪些呢?例如像運行時數據區的記憶體排布、垃圾收集演算法的使用,以及任何基於JVM指令集的內在優化等。這其中關於GC的部分是我們都比較熱衷的,將會額外開一篇進行學習。

③本文不會介紹Java不同版本的區別或特性升級,僅以目前工作中用到最多的java 8為學習材料。

④本文不會重點介紹javaSE的內容。

⑤class文件的編譯過程(*.java =javac=> *.class)可能不會包含在本文中。

最後補充一下,文章題目“Java有根兒”的由來及含義:“有根兒”通常指胸有成竹、有底氣、有靠山、自信的來源。這裡通過這種比較戲謔的詞語表達了Class文件以及類載入器對於Java的一個重要地位關係,同時也突出了娛樂時代,學習也是從興趣出發的一種心態,學習也是娛樂的一種 ^ ^。

關鍵字:JVM、Java、Class、位元組碼、BootstrapClassLoader、ClassLoader、雙親委派機制、熱部署

JVM前置知識

  1. JVM是Java的基石,但不限於Java語言使用,任何能夠生成class文件的語言皆可使用。

    實際上,JVM對Java語言一無所知,它只認識class文件,通過ClassLoader來載入,這是一種JVM特定的二進位文件,該文件包含了JVM指令、符號表以及一些附加信息。

  2. JVM是一個抽象電腦,有自己的指令集以及運行時記憶體操作區。

  3. JVM包括解釋器和JIT編譯器以及執行引擎,一般採用混合模式。編譯器會針對不同操作系統直接生成可執行文件,而解釋器是在運行時邊解釋邊執行。一般調用次數較多的類庫或程式會直接編譯成本地代碼,提高效率。

    編譯器和解釋器在其他語言也有廣泛的運用,總之活是一樣多,看你先乾還是後乾,各有利弊。純編譯器語言編譯的時候就慢但執行快,純解釋器語言編譯是很快,但執行稍慢。

  4. JVM對主流的不同操作系統都做了支持,JVM之上的語言層面不需要考慮操作系統的異構,繼而實現了語言的跨平臺。

  5. JRE包括JVM和JavaSE核心類庫。而JDK包括JRE和開發工具,包括核心類庫源碼等。一般作為開發者需要JDK,而運行Java程式只需要JRE即可。

1.class文件

class文件是JVM的輸入,內容是已編譯的代碼,它是一種跨硬體和跨操作系統的二進位格式。class文件可以準確定義類和介面,以及他們內部的針對不同平臺分配的記憶體位元組表示。下麵我們看一下一個class文件的16進位內容。

image-20220526191907223

圖1-A Class文件位元組碼

圖1-A是通過IDEA的BinEd插件,查看到的一個最簡單的類編譯出來的class文件的16進位內容,這個類源碼如下:

package com.evswards.jvm;
public class Test001 {}

由此我們能獲得一些信息:

  1. 每個位元組由兩個16進位數構成,每個16進位數我們知道是4位(bit),那麼一個位元組就是8位。class文件的最小描述單位就是8位的一個位元組,表現為16進位就是2個16進位數,所以圖中每兩個數要組合在一起不可分割。
  2. 按照每2個16進位數為最小單位來看,class文件的16進位格式有16列。圖1-A中是使用1個16進位數來表示每列的標號,其實也可以用十進位,但是由於列數固定在16,16進位看起來比較方便。
  3. 行數依據源代碼的內容大小而定,是不固定的。圖1-A中仍舊是使用16進位表示,好處是除去最右一位,剩下的位數可作為行數,而若算上最右一位,可作為整體位元組的個數。也相當於十進位的行數乘以列數的計算。

1.1 class文件結構

欄位 占位(byte) 值(參照圖1-A) Decimal 解釋
magic 4 0xCAFEBABE 不用記 與擴展名功能類似,但不可輕易修改
minor_version 2 0x0000 0 次版本號:不能低於該版本
major_version 2 0x0034 52 主版本號:即java 1.8,不能高於該版本
constant_pool_count 2 0x0010 16 常量池計數器長度為16
constant_pool ↑count-1 0x0A...626A656374 見1.2 ∵從#1開始,#0引用留做他用了∴長度-1
access_flags 2 0x0021 不用記 類訪問許可權public
this_class 2 0x0002 2 本類索引:#2【去常量池中找第2個】
super_class 2 0x0003 3 父類索引:#3「constant_pool」
interfaces_count 2 0x0000 0 源碼能看到就一個空類,沒聲明介面
interfaces ↑count 見1.3 ∵長度為0∴為空,不占用位元組
fields_count 2 0x0000 0 同樣沒聲明欄位
fields ↑count 見1.4 ∵長度為0∴為空,不占用位元組
methods_count 2 0x0001 1 有1個方法,是什麼呢?
methods 2||↑count 0x0001...000A0000 見1.5 其實是預設加的空構造函數
attributes_count 2 0x0001 1 有1個屬性信息
attributes 2||↑count 0x000B...0002000C 見1.6 記錄值SourceFile:Test001.java
表1-1-A Class文件結構

class文件結構中共有16個欄位,其中需要深研究的有常量池、介面、欄位、方法、屬性,後面逐一展開。

這裡field和attribute有點容易混淆,多聊兩句他們的區別:

1、先說class文件結構中的16個欄位,這種表述的理由是將class文件看成一個結構體,它的內容分類就是表1-1-A中列出16行內容。其中fields這一行也是class文件結構的欄位,但它也是class文件代表的類源碼Test001.java中我們顯示聲明的Java語言層面的欄位,例如:public String name;。

2、表1-1-A中的attributes這一行也是class文件結構的欄位,但它同時也是class文件代表的類源碼Test001.java文件的屬性,例如文件名。

1.2 常量池

JVM對於類、介面、類實例,以及數組的引用並不是在運行時完成的,而是通過class文件中的常量池來表示。常量池是一個數組,每條記錄都是由:

1、占用一個位元組的常量池標簽,例如CONSTANT_Methodref

2、對應的具體內容就是結尾加_info尾碼,例如CONSTANT_Methodref_info

所組成。先貼一個常量池標簽的對照表。

標簽類型(首碼CONSTANT_) 值(十進位) 轉換十六進位(1位元組) 解釋
Class 7 0x0007
Fieldref 9 0x09 欄位引用
Methodref 10 0x0A 方法引用
Interfacemethodref 11 0x0B 介面方法引用
String 8 0x08 字元串
Integer 3 0x03 整型
Float 4 0x04 單精度浮點
Long 5 0x05 長整型
Double 6 0x06 雙精度浮點
NameAndType 12 0x0C 名稱類型
Utf8 1 0x01 utf8字元串
MethodHandle 15 0x0F 方法處理
MethodType 16 0x10 方法類型
InvokeDynamic 18 0x12 動態調用
表1-2-A 常量池標簽對照表

至於表1-2-A為啥沒有2、13、14、17,不需要知道。。。

下麵,仍舊以圖1-A為例,參照表1-2-A,我們去嘗試解析表1-1-A中常量池的十六進位數據。首先先找正確答案,可通過IDEA的插件jclasslib Bytecode Viewer,分析class文件結構,其中常量池的部分如下圖1-2-A所示。

image-20220526235336200

圖1-2-A 位元組碼視圖插件

有了參考答案以後,我們去繼續解析表1-1-A中常量池的部分,它的值是圖1-A中的0x0A...626A656374部分。我們找到圖1-A中對應的部分,然後從0x0A開始往下解析:

1、0x0A是一個常量池標簽,對照表1-2-A,可以找到是CONSTANT_Methodref,它對應的具體內容是CONSTANT_Methodref_info。

2、通過官方JVM規範的4.4.2可查找到CONSTANT_Methodref_info。(把官方文檔當做字典來查是正確的打開方式。)看一下它的結構:

CONSTANT_Methodref_info {
       u1 tag;
       u2 class_index;
       u2 name_and_type_index;
   }

這是一個偽碼,主要看結構中的欄位,每個欄位前是位元組數,例如u1就是1個位元組,按照這個規範再回去跟蹤圖1-A的位元組碼。

3、0x0A本身就是1位元組的tag,再往後是2位元組的class_index,即0x0003,這是一個類索引,指向#3號的常量池記錄。

4、再往後是2位元組的name_and_type_index,即0x000D,這是一個名字和描述符,也是一個引用,執行#13的常量池記錄。

到此常量池的第一條記錄就解析完了,我們去看一下正確答案圖1-2-A的右側部分的內容,正好是與上面的分析對應上,證明我們的解析是正確的。

Bytecode Viewer

上面我們按照JVM規範逐一解析了class文件的16進位內容,解析的結果得到了驗證。JVM規範的本質就是在描述這件事,告知大家它是如何設定不同的區域所對應的位元組碼,如何通過這些位元組碼的規範去表示類、方法、欄位等等,由此可以支持非常複雜的信息化需求。其實就是一本翻譯書,我說”hello“,它告訴我是”打招呼,你好“的意思。前面驗證位元組碼的方式是通過IDEA的插件jclass Bytecode Viewer,那麼接下來就不用再費勁去比對十六進位了,直接通過插件來查看即可。接下來繼續分析。

1、前面分析到常量池的第一條記錄,表示的是方法引用,其中類名是#3,名字描述符是#13。首先看#3,在插件視圖中也可以直接點擊,跳轉過去更加方便。由於篇幅有限,這裡就不粘貼了,直接文字描述。

2、#3號常量池記錄是CONSTANT_Class_info,說明是類信息,它的值指向了#15。

3、#15號常量池記錄是CONSTANT_Utf8_info,說明是utf8字元串,長度是16,值是字面量:java/lang/Object。

4、回到1,我們已經知道了類名,繼續去查#13,#13是名字和描述符,其中名字指向#4,描述符指向#5。

5、#4也是字元串,長度為6,值是<init>

6、#5是字元串,長度為3,值是()V,代表的是參數為空,返回值為void。

好,到此我們總結一下,這個過程列出來,我們這個類由於內容為空,預設會添加父類的空構造函數,即Object類的構造函數init(),返回值是void。另外,我們也能夠發現,也不需要去跳轉查看,相關類信息或者各種數據類型的值都會在插件中顯示出來。這就更加方便了我們分析class文件的內容。我們在這個過程中已經把常量池中的一部分記錄所覆蓋到了,剩下的內容將在下麵的介面、欄位、方法以及屬性中會被引用到。

1.3 介面

由於圖1-A沒有介面的內容,我新寫一個介面,有了Bytecode Viewer插件,看起來比較方便了。

image-20220527225147114

圖1-3-A ①源碼-②位元組碼-③位元組碼分析

圖1-3-A顯示幾個信息:

1、①的部分是Test002的源碼,②的部分是位元組碼,③的部分是位元組碼視圖插件的顯示。

2、直接看③的部分,有疑議的可以參照①和②的部分。可以看到介面、欄位、方法、屬性都比較齊全。那麼下麵的分析都將以此為例。

本小節是分析介面的部分,這裡的介面指的是類源碼中實現的介面,參照①的部分,這裡實現了Cloneable介面。因此,可以在③的部分看到介面。介面項展開以後,有一條記錄,引用了#4號常量池。#4號常量池記錄是一個類信息,又指向了#19的字元串,最終顯示java/lang/Cloneable。這裡就不粘貼圖片了,可自行查看。

1.4 欄位

下麵看欄位的部分,還是通過查看③的區域,欄位有一條記錄,包括3個子項:

1、名字:指向#5常量池,對應的是一個字元串,值為<name>

2、描述符:指向#6常量池,對應的也是一個字元串<Ljava/lang/String>

3、訪問標誌:0x0002,是代表private的含義,與表1-1-A class文件結構中的access_flags的規則一致。

欄位的部分要註意對於源碼欄位的類型(descriptor_index),是用常量池的字元串來表示,例如private int age;欄位,也會在常量池中已utf8的方式存儲欄位的數據類型,這裡是int,存為utf8的字面量是I,String對應的是Ljava.lang.String,所有引用類型都是L加全限定類名。其他的映射關係是:byte->B, char->C, double->D, float->F, long->J, short->S, boolean->Z。

1.5 方法

方法的部分在本例中仍舊是預設添加的構造函數,這個內容在常量池的部分介紹到了。這裡再重申一下,方法有一條記錄,包括3個子項:

1、名字:引用#7常量池,值為<init>

2、描述符:引用#8常量池,值為<()V>

3、訪問標誌:0x0001,為public。

而往下深入查看,會發現在方法記錄中還有更深的層級,顯示的是[0]code

方法代碼

使用位元組碼視圖查看插件,可以看到[0]code包括一般信息和特有信息,一般信息就是將code以utf8保存在常量池。特有信息比較重要,這裡的是對應的空構造函數源碼,給出的位元組碼是:

`0 aload_0

1 invokespecial #1 <java/lang/Object. : ()V>

4 return`

這是JVM的指令集,要去規範文檔中查詢所代表的意思。

1、aload_0代表本地變數保存在記憶體中棧幀第0項,預設是this(下麵記憶體的部分會學習),位元組碼是0x2a,如果細心的話可以在圖1-3-A②的位元組碼中找到。

2、invokespecial代表調用實例方法,包括對於父類、私有以及實例初始化的處理。這裡指的是調用父類即Object的方法。

3、return返回void。

處了代碼的位元組碼以外,特有信息還包括異常表和雜項,不在這裡介紹了。

[0]code再往下還有更深一層,包括:

1、[0]LineNumberTable,代表源代碼行號

2、[1]LocalVariableTable,方法執行時本地變數的值

1.6 屬性

屬性包括一條名稱為SourceFile的記錄,包括一般信息和特有信息,一般信息就是記錄字元串”SourceFile“,特有信息就是源碼文件的實際名稱,Test002.java。

這裡要註意的是屬性也可以包括在欄位、方法中,也可以是整個class結構的屬性,他們的內容規範是一致的,只是取決於作用域。屬性是比較複雜的部分,上面提到的LineNumberTable和LocalVariableTable實際上都是屬性,code也是屬性(屬性信息本身作為一個事物也可以有自己的屬性,就像方法的屬性code也可以有自己的屬性LineNumberTable和LocalVariableTable),這種屬性的規範還有很多,JVM規範文檔中4.7的章節有詳細說明,在有用到的時候可以根據目錄快速查看。

2. ClassLoader

我們在第一章對Class文件的結構建立了初步印象。作為JVM的輸入,class文件在進入JVM的第一關就是通過ClassLoader也就是類載入器將Class靜態文件中的位元組碼解析並載入到JVM記憶體中。本章就介紹類載入器ClassLoader。

JVM會動態的對類和介面進行載入、鏈接以及初始化。載入是一個過程,為一個類或介面類型的二進位文件找到一個特定的名字並從該二進位描述中創建一個類或介面。鏈接是另外一個過程,拿到一個類或介面,將其合併到JVM運行時狀態中,由此它才可以被執行。最後,一個類或介面的初始化,其實就是執行類或介面的初始化方法,例如構造函數。

JVM的啟動過程:①通過bootstrap類載入器創建一個入口類。②鏈接該入口類、初始化,然後調用public的main方法。③main方法驅動所有其他的遠程執行,按照這個執行時機,所關聯到的其他類或介面都會被逐一載入、創建、鏈接以及初始化,包括他們的方法。(有一些JVM的實現,會將入口類作為JVM命令行啟動的參數,或者有固定的入口類設定。

2.1 雙親委派

類載入器並不是一個,而是多個,按照順序,他們是父子載入器的關係:

1、Bootstrap

2、Extension

3、App

4、Custom ClassLoader

其中最為基礎的是Bootstrap類載入器,它是JVM內置的由C++所編寫的,固定地用來載入核心類庫到JVM運行時,這是操作系統級別的代碼。接下來是Extension擴展類載入器,載入擴展包jre/lib/ext/*.jar,或者由-Djava.ext.dirs參數來指定類載入路徑。接下來是App,載入classpath指定的內容。最後是自定義類載入器,對於我們JVM的使用者來講,這部分是應用最多的。

下麵學習雙親委派的概念。

當一個類要被載入到JVM的時候,會自底向上的查找是否載入過。 首先是自定義類載入器,找不到的話再向上去查App類載入器,接著是Extension,最後到Bootstrap。如果都沒有找到,則需要觸發類載入。類載入的過程是自頂向下的。Boostrap首先會執行載入的方法findClass(),但它不會載入核心類庫以外的類,所以會往下傳遞到Extension。如果這個類不在Extension載入的findClass()邏輯覆蓋,則它也不會載入,會往下繼續傳給App。同樣的,App類載入器也有自己的findClass(),如果也不在邏輯內,則繼續傳給自定義類載入器。如果自定義類載入器也沒有開發相關的邏輯,即重寫findClass(),這個類就會被丟棄,不再載入。而一般情況下,我們會在自定義類載入器中去重寫findClass()處理要自定義載入的類的邏輯。

這個載入過程就用到了雙親委派,前面提到了這4個類載入器按照順序是父子層級關係,因此一個新類的載入,需要孩子向父親方向逐層查找,然後再從父親向孩子方向逐層載入的過程。這就是雙親委派。

雙親委派的意義

前面講到了,4中類載入器有各自不同的實現和許可權,那麼雙親委派的過程實際上就對新載入類進行了層層校驗,以避免底層類庫被替換的情況發生,所以主要是從安全形度考慮而設計的。

2.2 ClassLoader源碼

進入java.lang.ClassLoader類源碼中,首先看它的類註釋。第一段概況性描述了ClassLoader的功能,本質就是在系統中定位到class文件並讀入進來,這個過程中做了一些處理,例如安全、併發(多線程情況下去執行類載入的策略,為保證不會重覆載入,會加鎖,通過registerAsParallelCapable()方法),以及IO(class文件不再是狹隘的系統中的一個文件,而是一個二進位文件流,它的來源可以是本地文件也可以是網路傳輸。通過defineClass()方法讀入)。

1、首先ClassLoader類是一個抽象類,定義了一個類載入器的規範,它的子類包括了SecureClassLoader、RBClassLoader、DelegatingClassLoader等,包括我們自己實現的子類也屬於直屬於java.lang.ClassLoader的子類。

2、Java語言裡面,類型的載入是在程式運行期間完成的,也就是說用到的時候再創建,而不是在程式編譯時或者啟動時就把所有的對象準備好,這一點常用Java的人應該瞭解。這種策略是與其他語言稍有不同的,雖然會令類載入時增加一些性能開銷,但會提高Java應用程式的靈活性。

Java里天生可以動態擴展的語言特性就是依賴運行期動態載入這個特點實現的。(包括動態的鏈接,後面會學習到)。這種動態載入也被稱為懶載入。

3、根據以上2點,可以得知ClassLoader子類會在使用到的時候去創建實例,那麼核心類載入器的創建時機是什麼呢?其實在上面的JVM啟動過程中提到了,指定入口類的main方法作為整個JVM運行的開始,會執行Launcher類,該類是ClassLoader的包裝類,其中包括了前面提到的Bootstrap類載入器、Extension類載入器以及App類載入器,那麼剩下的自定義類載入器其實就是第一點中提到的java.lang.ClassLoader的子類,按照動態載入策略被載入進來。

下麵我們進入源碼的學習。

父類載入器

private final ClassLoader parent;

每一個類載入器都會有一個類載入器對象作為屬性,屬性名稱是parent,這就是父類載入器,它是final的,即定義好就不可修改。由於該父類載入器是一個成員屬性,所以要與繼承的父類概念相區分。當然,它也不是當前類載入器的創建者。

並行載入器類

private static final Set<Class<? extends ClassLoader>> loaderTypes =
    Collections.newSetFromMap(
        new WeakHashMap<Class<? extends ClassLoader>, Boolean>());

接下來是一個並行載入器類,該類中包含一個如上面粘貼的源碼內容的Set集合,該集合的元素只能是ClassLoader的子類,它的數據結構是由一個WeakHashMap類型轉型過來的集合。該WeakHashMap類型的key是ClassLoader子類(註意不是對象),value是Boolean類型。預設在靜態方法中會初始加入ClassLoader類。

靜態方法:該並行載入類定義好上面這個記憶體結構以後,又給出了註冊register(子類)以及判斷是否註冊isRegistered(子類)的方法。其中都包含了針對併發的synchronized處理。register方法會在registerAsParallelCapable()方法中被使用到。registerAsParallelCapable()方法在類註釋中提到過,主要是為了並行。

loadClass方法

類載入器最重要的是載入方法,loadClass方法就是核心方法,這個方法的源碼就粘貼完整一些。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

1、檢查該類是否已經被載入,通過findLoadedClass()方法(該方法最終實現指向了native方法,是系統級別方法,可能不是java寫的,無源碼)。如果查到已被載入則執行解析邏輯resolve(解析的最終實現也是個native方法),再直接返回。

2、若該類未被載入,檢查父類載入器是否存在,若不存在則去查找Bootstrap類載入器中是否存在(最終實現也是個native方法),不存在會返回null。

3、若父類載入器存在,則當前子類載入器的loadClass方法阻塞在這裡,線程轉而去執行父類載入器的loadClass方法。父類載入器同樣也是ClassLoader類的子類,loadClass方法的代碼是相同的,因此它也會執行到這裡仍舊去查是否存在它的父類載入器。就像執行一個遞歸函數那樣以此類推。

4、程式會執行直到沒有父類載入器的最底層類載入器,我們前面介紹到了,就是Bootstrap類載入器,它是沒有父類載入器的,因此通過findBootstrapClassOrNull(name)方法來查詢。這個方法的最終實現同樣要指向native本地代碼,如果找到則返回Class類,未找到則返回null。到此我們的遞歸函數開始收攏。

5、Boostrap類載入器的一級子類載入器會得到前者的返回值,如果找到了,則執行解析邏輯resolve,再直接返回。

6、如果沒找到,則往下執行findClass方法。該方法是每一個ClassLoader子類都會重寫的方法,如果找不到仍舊會繼續往上返回給自己的子類null。遞歸函數繼續收攏。

7、繼續找,直到在某一層級的子類載入器中找到了,則執行解析邏輯resolve,再直接返回。如果最終整個遞歸函數已經收攏迴首層也沒有找到,會有兩種可能。第一、直接返回null。第二,就是過程中某一層類載入器顯式拋出了ClassNotFoundException異常,被下一層的孩子捕捉到了以後做了處理。註意,這個過程我們在ClassLoader源碼中可以看到一個框架結構,但並沒有具體實現,這是留給子類去發揮的地方。

總結一下,我們會發現整個這個過程通過parent父類載入器以及loadClass方法的代碼邏輯,完成了對於雙親委派策略的實現。

findClass方法

前面在loadClass方法的源碼分析中,在遞歸調用的各級類載入器的邏輯中,他們對於ClassLoader類的findClass方法的重寫內容顯得至關重要。由於子類非常多,也包括在jdk以外的子類實現,我們挑選到URLClassLoader類的源碼作為研究對象,看一下它的findClass方法是如何重寫的。

protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

這個源碼的邏輯簡單介紹一下。

1、參數約定傳入的是全限定類名,因此首先要對參數進行改造,得到它的文件路徑。

2、然後通過getResource獲得文件的Resource對象。

3、最後調用defineClass獲得類返回值。

defineClass方法

還是由前面的findClass方法繼續分析,一路追蹤到defineClass方法。首先來看它的入參,除了傳遞了全限定類名的字元串以外,還傳入了Resource對象。核心的代碼如下:

java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
    // Use (direct) ByteBuffer:
    CodeSigner[] signers = res.getCodeSigners();
    CodeSource cs = new CodeSource(url, signers);
    sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
    return defineClass(name, bb, cs);
} else {
    byte[] b = res.getBytes();
    // must read certificates AFTER reading bytes.
    CodeSigner[] signers = res.getCodeSigners();
    CodeSource cs = new CodeSource(url, signers);
    sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
    return defineClass(name, b, 0, b.length, cs);
}

這裡首先定義了一個nio包的ByteBuffer對象bb,然後有兩個分支。如果bb有值,則直接使用ByteBuffer數據結構。如果bb為空,則讀出它的位元組碼,然後去調用另一個入參為位元組碼的defineClass方法。其實直接使用ByteBuffer的分支跟蹤進去最終也會調用這個入參為位元組碼的defineClass方法,這個方法的最終實現也是native本地方法,實現細節我們不得而知,除非去分析C++源碼。對於defineClass我們只要知道,不僅是文件路徑,只要是能轉為位元組碼的格式,類載入器都支持。

雙親委派機制的打破

前面仔細介紹了類載入過程中的雙親委派機制,主要是在ClassLoader的loadClass方法中固定實現的,那麼有沒有情況是要打破這個機制的呢?答案是有的,當我們希望類的載入可以實現對JVM現有的類進行替換的時候。我們知道在雙親委派機制下,重覆的類不會被載入進來,因為會自底向上去查詢,一旦查到JVM已經載入過了,就直接返回而不會再載入你新準備覆蓋傳入的同名類。

所以對應的實現方法就是我們自定義的類載入器不能僅僅去重寫findClass方法了,而是要重寫loadClass方法,把其中向上查找,找到就返回的邏輯給去掉。修改為找到Class文件,不再去判斷是否有同名。

Tomcat的底層實現就是基於對雙親委派機制的打破以及垃圾回收的結合應用,從而實現了熱部署,也即在不停機的情況下對代碼進行更新操作。那麼具體是如何實現的呢?這裡不做tomcat源碼級別的學習,而是說一個原理:

1、重寫loadClass方法,去除雙親委派的查找邏輯,也就是允許同名的類載入進來。

2、然後同名類在載入的時候,不再使用原來的類載入器的實例,而是新創建一個實例來載入。

3、這時候,JVM記憶體中是存在兩個類載入器的實例,他們各自都載入了一個同名的類。

4、此時,再通過Java垃圾回收機制,通過判定標記,將舊的類載入器實例進行主動銷毀。

5、這時候記憶體中就只留下最新的類了,實現了不停機的一個代碼替換。

不過這裡也有很多細節問題需要研究tomcat源碼去完善,例如類載入器實例不僅僅載入了這一個類,還有很多未更新的類在新的實例創建的時候也要同時再載入一遍進來,這個邏輯的具體實現。還有像新創建一個類載入器的實例的機制,實例是如何被管理的,以及具體的判定舊實例的過時和銷毀等等。

2.3 Launcher源碼

前面提到了Bootstrap、Extension以及App類載入器的層級關係,那麼他們是如何定義的,JVM在啟動時是如何初始化類載入器的,其實答案都在Launcher類中。

private ClassLoader loader;

1、Launcher類是ClassLoader的包裝類,它有一個ClassLoader的成員。

2、接著,它定義了Bootstrap、Extension以及App類載入器的文件掃描路徑,這些路徑可以通過JVM啟動參數手動指定,但啟動以後就不可修改(不包括熱部署的情況)。

3、Launcher類包含了內部類APPClassLoader、BootClassPathHolder、ExtClassLoader分別對應以上三種類載入器,這裡面與其他不同的是Bootstrap類載入器並不是ClassLoader而是PathHolder。Bootstrap類載入器,前面提到它是C++編寫到操作系統的本地類庫,因此它的具體實現並不是java.lang.ClassLoader的子類。這裡只是通過它來確定文件路徑sun.boot.class.path的邏輯。

4、其他兩個類載入器都是ClassLoader的子類,具體來說是URLClassLoader的子類,URLClassLoader我們在前面的findClass方法的重寫部分做了充分研究。這裡的兩個類載入器在URLClassLoader的基礎上,做了一些針對自己功能責任的調整。

2.4 findClass方法的妙用

前面詳細學習了findClass方法,ClassLoader的子類包括我們自定義的類載入器都會去重寫該方法。那麼通過對該方法的內容實現的靈活使用,可以實現一些特殊的功能。例如Class文件的加密。我們可以給自己的源碼編譯出來的Class文件進行加密,Class文件是一個二進位文件,可以通過位運算或其他加密演算法的邏輯運算把原始位元組加密成密文位元組。所謂的密文位元組其實就是通用的解析方式不再適配了,這個通用的解析方式其實就是前面介紹的JVM規範。那麼我們自己如何進行載入呢?可以通過重寫findClass方法,因為我們知道自己Class位元組碼的加密方式,所以可以在findClass方法中寫入自己的解密邏輯,從而就實現了源碼的加密保護,只有我自己可以載入,而其他人只要不清楚我的加密方式以及加密種子,就不會完成加密類文件的一個正常載入,直接反編譯也會顯示亂碼。

參考資料

更多文章請轉到一面千人的博客園


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 主要記錄工作中用到的一些開發語言以及Sql 模板,持續更新 1.Sql相關 1.常用Sql模板 1.1. 可重覆執行視圖 IF EXISTS ( SELECT *FROM sysobjects WHERE id = OBJECT_ID('v_Employee') AND type = 'V' ) D ...
  • 下表數據,是歷時四年,不定期記錄下的本博積分與排名情況。 咋一看,是個挺簡單的數據表,似乎依此可以輕鬆地搞出個增長曲線圖之類的東東,再分析點什麼結論出來。但再仔細研究一下,發現不那麼回事,這裡面還是挺複雜的。 突然有種感覺,這和軟體開發何期的相似。有多少次,剛聽到一個新需求時,會感覺這不是個什麼事, ...
  • 緊急更新第二彈,然後就剩下最後一彈,也就是整個前臺的項目 一.購物車 1.加入購物車(新知識點) 加入到購物車是需要介面操作的,因為我們需要將用戶的加入到購物車的保存到伺服器資料庫,你的賬號後面才會在你自己的購物車看到,所以這裡要先寫介面 然後vuex三部曲, 返回來的數據沒有data,就是告訴你成 ...
  • jQuery jQuery是什麼 jQuery是一個快速、簡潔的JavaScript框架,是繼Prototype之後又一個優秀的JavaScript代碼庫(或JavaScript框架)。jQuery設計的宗旨是“write Less,Do More”,即倡導寫更少的代碼,做更多的事情。它封裝Java ...
  • 這幾天一直都在做項目,只是沒有上傳上來,即將把前臺項目完結了。現在開始更新整個前臺的部分 一.麵包屑處理 1.分類操作 點擊三級聯動進入搜索產生麵包屑,直接取參數中的name即可 點擊x怎麼幹掉這個麵包屑,直接讓其v-if為這個name,如果點擊x就把name清空 清空還沒完,清空應該再發一次請求, ...
  • 本章是系列文章的第五章,介紹了指針分析方法。指針分析在C/C++語言中非常重要,分析的結果可以有效提升指針的優化效率。 本文中的所有內容來自學習DCC888的學習筆記或者自己理解的整理,如需轉載請註明出處。周榮華@燧原科技 5.1 概念 指針是許多重要編程語言的特性之一 指針的使用,可以避免大量的數 ...
  • 第四章: 設計與聲明 ###18. 讓介面更容易被正確使用,不易被誤用 將你的class的public介面設計的符合class所扮演的角色,必要時不僅對傳參類型限制,還對傳參的值域進一步限制。 ###19. 設計class猶如設計type 內置類型如int、float等,本質也是一個class,用戶 ...
  • 前言 還記得你第一次遇到「線程安全」這個詞的時候嗎? 我第一次遇到線程安全這個詞是在學習多線程併發操作的時候,看到人家文章里出現這個詞,還有說各種線程安全的類,但是一開始並不理解線程安全是什麼意思,也沒去深究線程怎樣是安全的?怎樣是不安全的?只是腦子裡接收了這麼一個詞。 線程安全是多線程編程時的計算 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...