主頁 > 後端開發 > Java有根兒:Class檔案以及類加載器

Java有根兒:Class檔案以及類加載器

2022-05-30 20:35:40 後端開發

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 0x07
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.<init> : ()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方法中寫入自己的解密邏輯,從而就實作了原始碼的加密保護,只有我自己可以加載,而其他人只要不清楚我的加密方式以及加密種子,就不會完成加密類檔案的一個正常加載,直接反編譯也會顯示亂碼,

參考資料

  • *JVM 1.8官方檔案

  • java SE 1.8官方檔案

  • 《JVM調優》馬士兵

  • 《深入理解Java虛擬機》周志明

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/482766.html

標籤:其他

上一篇:Angular的.navigateByUrl()導航到頁面,但沒有呈現html

下一篇:執行緒安全,這詞你懂了嗎?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more