柚子快報邀請碼778899分享:JVM類加載
柚子快報邀請碼778899分享:JVM類加載
JVM之走進類加載
計算機體系結(jié)構(gòu)
遵循馮諾依曼計算機結(jié)構(gòu)
計算機處理數(shù)據(jù)過程
(1)提取階段:由輸入設備把原始數(shù)據(jù)或信息輸入給計算機存儲器存起來
(2)解碼階段:根據(jù)CPU的指令集架構(gòu)(ISA)定義將數(shù)值解譯為指令
(3)執(zhí)行階段:再由控制器把需要處理或計算的數(shù)據(jù)調(diào)入運算器
(4)最終階段:由輸出設備把最后運算結(jié)果輸出
本質(zhì)上就是CPU取數(shù)據(jù)指令然后返回
CPU=存儲器+運算器+控制器
機器語言
我們把CPU能夠直接認識的數(shù)據(jù)指令,稱為機器語言,也就是010101001這種形式
不同廠商的CPU
單核、雙核、多核
Intel、AMD、IBM等
不同CPU使用的CPU指令集是不一樣的,這就會有不兼容的問題
而且要是直接操作01這種形式的,非常麻煩并且容易出錯,硬件資源管理起來也不方便
操作系統(tǒng)
向下對接指令系統(tǒng)、管理硬件資源向上提供給用戶簡單的操作命令和界面
匯編語言
低級語言,通過匯編器翻譯成機器語言
MOV、PUSH、ADD等
對機器友好,執(zhí)行效率比較高,移植性差
但是人類操作起來還是不太方便,或者需要專業(yè)的人員
高級語言
C、C++、Java、Python、Golang等
最終肯定還是要轉(zhuǎn)換成機器能夠懂的機器語言
編譯型和解釋型
編譯型
使用專門的編譯器,針對特定的平臺,將高級語言源代碼一次性的編譯成可被該平臺硬件執(zhí)行的機器碼,并包裝成該平臺所能識別的可執(zhí)行性程序的格式。
C、C++、GoLang
編譯型語言:
執(zhí)行速度快、效率高;依靠編譯器、跨平臺性差些。
把做好的源程序全部編譯成二進制代碼的可運行程序。然后,可直接運行這個程序。
解釋型
**使用專門的解釋器對源程序逐行解釋成特定平臺的機器碼并立即執(zhí)行。是代碼在執(zhí)行時才被解釋器一行行動態(tài)翻譯和執(zhí)行,而不是在執(zhí)行之前就完成翻譯。 **
Python、Javascript
解釋型語言:
執(zhí)行速度慢、效率低;依靠解釋器、跨平臺性好。
把做好的源程序翻譯一句,然后執(zhí)行一句,直至結(jié)束。
Java呢?
Java屬于編譯型+解釋型的高級語言
其實并不是因為有javac將Java源碼編譯成class文件,才說Java屬于編譯+解釋語言,因為在這個編譯器編譯之后,生成的類文件不能直接在對應的平臺上運行。
那為何又說Java是編譯+解釋語言呢?因為class文件最終是通過JVM來翻譯才能在對應的平臺上運行,而這個翻譯大多數(shù)時候是解釋的過程,但是也會有編譯,稱之為運行時編譯,即JIT(Just In Time)。
綜上所述,Java是一門編譯型+解釋型的高級語言。
1.4 So JVM是什么?
JVM(Java虛擬機)是一種在計算機上運行Java字節(jié)碼的虛擬機。它是Java編程語言的核心,提供了一個運行環(huán)境,用于執(zhí)行Java程序。JVM的主要功能包括字節(jié)碼解釋、垃圾回收、即時編譯等。
JVM的工作原理是將Java源代碼編譯成字節(jié)碼文件(.class文件),然后由JVM解釋執(zhí)行字節(jié)碼。JVM具有跨平臺的特性,即一次編譯,到處運行。這是因為JVM在不同的操作系統(tǒng)上都提供了相應的實現(xiàn),可以通過安裝Java運行時環(huán)境(JRE)來在不同的操作系統(tǒng)上運行Java程序。
JVM還具有內(nèi)存管理的功能,包括堆內(nèi)存和棧內(nèi)存。堆內(nèi)存用于存儲對象實例,棧內(nèi)存用于存儲方法調(diào)用和局部變量。JVM通過垃圾回收機制自動管理堆內(nèi)存中不再使用的對象,減輕了程序員手動釋放內(nèi)存的負擔。
另外,JVM還支持即時編譯(JIT)技術(shù),可以將熱點代碼(頻繁執(zhí)行的代碼)編譯成本地機器碼,提高程序的執(zhí)行效率。
總而言之,JVM是Java程序的運行環(huán)境,負責解釋執(zhí)行字節(jié)碼、管理內(nèi)存和提供一些額外的功能,使得Java程序可以在不同的操作系統(tǒng)上運行。
JVM是在計算機上運行java字節(jié)碼的虛擬機,為java程序提供了一個運行環(huán)境。jvm的工作原理是將java代碼編譯成字節(jié)碼文件去執(zhí)行,可以在不同的操作系統(tǒng)上去執(zhí)行,也就是一次編譯,到處運行。支持他這么做的原因就是JRE。JVM的功能包括字節(jié)碼解釋、垃圾回收(內(nèi)存管理**)等**。
Java Virtual Machine(Java虛擬機)
Write Once Run Anywhere
1.5 JDK JRE JVM
Java官網(wǎng):https://docs.oracle.com/javase/8/
Reference -> Developer Guides -> 定位到:https://docs.oracle.com/javase/8/docs/index.html
JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.
JDK 8 是 JRE 8 的超集,包含 JRE 8 中的所有內(nèi)容,以及程序所需的編譯器和調(diào)試器等工具。
JRE 8 提供了庫、Java 虛擬機 (JVM) 和其他組件,用于運行用 Java 編程語言編寫程序。請注意,JRE 包括 Java SE 規(guī)范不需要的組件,包括標準和非標準 Java 組件。
JVM到底該學習什么
(1)源碼到類文件
(2)類文件到JVM
(3)JVM各種折騰[內(nèi)部結(jié)構(gòu)、執(zhí)行方式、垃圾回收、本地調(diào)用等]
源碼到類文件
源碼demo
package com.example.jvmcase.basic;
class Person {
private String name = "yzt";
private int age;
private final double salary = 100;
private static String address;
private final static String hobby = "Programming";
private static Object obj = new Object();
public void say() {
System.out.println("person say...");
}
public static int calc(int op1, int op2) {
op1 = 3;
int result = op1 + op2;
Object obj = new Object();
return result;
}
public static void main(String[] args) {
calc(1, 2);
}
}
編譯: javac -g:vars Person.java —> Person.class
前期編譯
**Person.java -> 詞法分析器 -> tokens流 -> 語法分析器 -> 語法樹/抽象語法樹 **
-> 語義分析器 -> 注解抽象語法樹 -> 字節(jié)碼生成器 -> Person.class文件
2.1.3 類文件(Class文件)
2.1.3.1 16進制
2.1.3.2 The ClassFile Structure
官網(wǎng): https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
ClassFile {
? u4 ? ? ? ? ? ? magic;
? u2 ? ? ? ? ? ? minor_version;
? u2 ? ? ? ? ? ? major_version;
? u2 ? ? ? ? ? ? constant_pool_count;
? cp_info ? ? ? constant_pool[constant_pool_count-1];
? u2 ? ? ? ? ? ? access_flags;
? u2 ? ? ? ? ? ? this_class;
? u2 ? ? ? ? ? ? super_class;
? u2 ? ? ? ? ? ? interfaces_count;
? u2 ? ? ? ? ? ? interfaces[interfaces_count];
? u2 ? ? ? ? ? ? fields_count;
? field_info ? ? fields[fields_count];
? u2 ? ? ? ? ? ? methods_count;
? method_info ? methods[methods_count];
? u2 ? ? ? ? ? ? attributes_count;
? attribute_info attributes[attributes_count];
}
2.1.3.3 Simple analysis
u4 :cafebabe
magic:The magic item supplies the magic number identifying the class file format
u2+u2:0000+0034,34等于10進制的52,表示JDK8
minor_version
major_version
u2:003f=63(10進制)
constant_pool_count:
The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.
表示常量池中的數(shù)量是62
cp_info constant_pool[constant_pool_count-1]
The constant_pool is a table of structures representing various string constants, class and interface names, field names, and other constants that are referred to within the ClassFile structure and its substructures. The format of each constant_pool table entry is indicated by its first "tag" byte.
The constant_pool table is indexed from 1 to constant_pool_count - 1.
常量池主要存儲兩方面內(nèi)容:字面量(Literal)和符號引用(Symbolic References)
字面量:文本字符串,final修飾等
符號引用:類和接口的全限定名、字段名稱和描述符、方法名稱和描述符
2.1.3.4 javap驗證
JDK自帶的命令
javap -h
可以驗證一下上述Classfile Structure前面幾塊內(nèi)容的正確性
javap -v -p Person.class 進行反編譯,查看字節(jié)碼信息和指令等信息
是否有一種感覺?
JVM相對class文件來說可以理解為是操作系統(tǒng);class文件相對JVM來說可以理解為是匯編語言或者機器語言。
2.1.3.5 Continous analysis
上面分析到常量池中常量的數(shù)量是62,接下來我們來具體分析一下這62個常量
cp_info constant_pool[constant_pool_count-1] 也就是這塊包括的信息
cp_info其實就是一個表格的形式
**All **constant_pool table entries have the following general format:
cp_info {
u1 tag;
u1 info[];
}
官網(wǎng):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
(1)往下數(shù)一個u1,即0a->10:代表的是CONSTANT_Methodref,表示這是一個方法引用
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
往下數(shù)u2和u2
u2,即00 0a->10:代表的是class_index,表示該方法所屬的類在常量池中的索引 u2,即00 2b->43:代表的是name_and_type_index,表示該方法的名稱和類型的索引
#1 = Methodref ? ? ? #10,#43
(2)往下數(shù)u1,即08->8:表示的是CONSTANT_String,表示字符串類型
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
往下數(shù)u2
u2,即00 2c->44:代表的是string_index
#1 = Methodref ? ? ? #10,#43
#2 = String ? ? ? ? #44
(3)往下數(shù)u1,即09->9:表示的是CONSTANT_Fieldref,表示字段類型
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
往下數(shù)u2和u2
u2,即00 0d->13:代表的是class_index u2,即00 2d->45:代表的是name_and_type_index
#1 = Methodref ? ? ? #10.#43
#2 = String ? ? ? ? #44
#3 = Fieldref ? ? ? #13.#45
類文件到虛擬機(類加載機制)
類加載機制是指我們將類的字節(jié)碼文件所包含的數(shù)據(jù)讀入內(nèi)存,同時我們會生成數(shù)據(jù)的訪問入口的一種
特殊機制。那么我們可以得知,類加載的最終產(chǎn)品是數(shù)據(jù)訪問入口。
這個時候,看到這張圖,我們應該有一個問題,那就是我們的字節(jié)碼加載的方式,也就是我們的字節(jié)碼文件可以用什么方式進行加載呢?
加載.class文件的方式
從本地系統(tǒng)中直接加載
典型場景:這個我就不廢話了
通過網(wǎng)絡下載.class文件
典型場景:Web Applet,也就是我們的小程序應用
從zip,jar等歸檔文件中加載.class文件
典型場景:后續(xù)演變?yōu)閖ar、war格式
從專有數(shù)據(jù)庫中提取.class文件
典型場景:JSP應用從專有數(shù)據(jù)庫中提取.class文件,較為少見
將Java源文件動態(tài)編譯為.class文件,也就是運行時計算而成
典型場景:動態(tài)代理技術(shù)
從加密文件中獲取,
典型場景:典型的防Class文件被反編譯的保護措施
好,聊完了這個問題之后,問題接踵而至,我們的類加載的方式已經(jīng)了解了,那么加載的流程到底是怎
樣的呢?
所謂類加載機制就是
虛擬機把Class文件加載到內(nèi)存
并對數(shù)據(jù)進行校驗,轉(zhuǎn)換解析和初始化
形成可以虛擬機直接使用的Java類型,即java.lang.Class
裝載(Load)
查找和導入class文件
(1)通過一個類的全限定名獲取定義此類的二進制字節(jié)流(由上可知,我們不一定從字節(jié)碼文件中獲得,還有上述很多種方式)
思考:那么這個時候我們是不是需要一個工具,尋找器,來尋找獲取我們的二進制字節(jié)流。而我們的java中恰好有這么一段代碼模塊??梢詫崿F(xiàn)通過類全名來獲取此類的二進制字節(jié)流這個動作,并且將這個動作放到放到java虛擬機外部去實現(xiàn),以便讓應用程序決定如何獲取所需要的類,實現(xiàn)這個動作的代碼模塊成為“類加載器” 。
(2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu) (3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口
獲取類的二進制字節(jié)流的階段是我們JAVA程序員最關注的階段,也是操控性最強的一個階段。因為這個階段我 們可以對于我們的類加載器進行操作,比如我們想自定義類加載器進行操作用以完成加載,又或者我們想通過 JAVA Agent來完成我們的字節(jié)碼增強操作。
在我們的裝載階段完成之后,這個時候在我們的內(nèi)存當中,我們的運行時數(shù)據(jù)區(qū)的方法區(qū)以及堆就已經(jīng)有數(shù)據(jù)了。
分別在方法區(qū)和堆中生成了一個對象,InstanceKlass 和 java.lag.class
方法區(qū):基本信息,常量池,字段,方法,虛方法表(實現(xiàn)多態(tài)的基礎)。這些方法區(qū)中的統(tǒng)稱為 InstanceKlass(C++)(第1、2步完成)堆:代表被加載類的java.lang.Class對象,有字段,方法等信息。(第3步完成)
即時編譯之后的熱點代碼并不在這個階段進入方法區(qū)
鏈接(Link)
驗證(Verify)
驗證只要是為了確保Class文件中的字節(jié)流包含的信息完全符合當前虛擬機的要求,并且還要求我們的信息不會危害虛擬機自身的安全,導致虛擬機的崩潰。
文件格式驗證
驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)之內(nèi)。這階段的驗證是基于二進制字節(jié)流進行的,只有經(jīng)過該階段的驗證后,字節(jié)流才會進入內(nèi)存的方法區(qū)中進行存儲,后面驗證都是基于方法區(qū)的存儲結(jié)構(gòu)進行的。
舉例:
1.是否以16進制cafebaby開頭 2.版本號是否正確
元數(shù)據(jù)驗證
對類的元數(shù)據(jù)信息進行語義校驗(其實就是對Java語法校驗),保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。
舉例:
1.是否有父類
2.是否繼承了final類
因為我們的fifinal類是不能被繼承的,繼承了就會出現(xiàn)問題。
3.一個非抽象類是否實現(xiàn)了所有的抽象方法
如果沒有實現(xiàn),那么這個類也是無效的。
總結(jié):對類的元數(shù)據(jù)信息進行語義校驗(其實就是對Java語法校驗),保證不存在不符合Java語法規(guī)范的元數(shù)據(jù)信息。
字節(jié)碼驗證
進行數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。對類的方法體進行校驗分析,保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。獲取類的二進制字節(jié)流的階段是我們JAVA程序員最關注的階段,也是操控性最強的一個階段。因為這個階段我們可以對于我們的類加載器進行操作,比如我們想自定義類加載器進行操作用以完成加載,又或者我們想通過JAVA Agent來完成我們的字節(jié)碼增強操作。
舉例:
字節(jié)碼的驗證會相對來說較為復雜 。
1.運行檢查
2.棧數(shù)據(jù)類型和操作碼操作參數(shù)吻合(比如??臻g只有4個字節(jié),但是我們實際需要的遠遠大于4個字節(jié),那么這個時候這個字節(jié)碼就是有問題的)
3.跳轉(zhuǎn)指令指向合理的位置
符號引用驗證
這是最后一個階段的驗證,它發(fā)生在虛擬機將符號引用轉(zhuǎn)化為直接引用的時候(解析階段),可以看作是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。符號引用驗證的目的是確保解析動作能正常執(zhí)行。
舉例:
1.常量池中描述類是否存在
2.訪問的方法或者字段是否存在且具有足夠的權(quán)限
注意:但是,我們很多情況下可能認為我們的代碼肯定是沒問題的,驗證的過程完全沒必要,那么其實我們可
以添加參數(shù)
-Xverify:none 取消驗證。
準備(Prepare)
為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認值
private final static int a = 1 ; a = 0;
這里不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯式初始化;這里不會為實例變量(也就是沒加static)分配初始化,類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到Java堆中。
public class Demo1 {
private static int i;
public static void main(String[] args) {
// 正常打印出0,因為靜態(tài)變量i在準備階段會有默認值0
System.out.println(i);
}
}
public class Demo2 {
public static void main(String[] args) {
// 編譯通不過,因為局部變量沒有賦值不能被使用
int i;
System.out.println(i);
}
}
進行分配內(nèi)存的只是包括類變量(靜態(tài)變量),而不包括實例變量,實例變量是在對象實例化時隨著對象一起分配在java堆中的。
通常情況下,初始值為零值,假設public static int a=1;那么a在準備階段過后的初始值為0,不為1,這時候只是開辟了內(nèi)存空間,并沒有運行java代碼,a賦值為1的指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以a被賦值為1是在初始化階段才會執(zhí)行。
對于一些特殊情況,如果類字段屬性表中存在ConstantValue屬性,那在準備階段變量a就會被初始化為ContstantValue屬性所指的值。對于這句話,我們又怎么理解呢?
我們可以看一看我們反編譯之后的文件,我們就可以發(fā)現(xiàn)有這樣一個屬性。
思考:ConstantValue屬性到底是干什么的呢?
ConstantValue屬性的作用是通知虛擬機自動為靜態(tài)變量賦值,只有被static修飾的變量才可以使用這項屬性。非static類型的變量的賦值是在實例構(gòu)造器方法中進行的;static類型變量賦值分兩種,在類構(gòu)造其中賦值,或使用ConstantValue屬性賦值。
思考:在實際的程序中,我們什么時候才會用到ContstantValue屬性呢?
在實際的程序中,只有同時被final和static修飾的字段才有ConstantValue屬性,且限于基本類型和String。編譯時Javac將會為該常量生成ConstantValue屬性,在類加載的準備階段虛擬機便會根據(jù)ConstantValue為常量設置相應的值,如果該變量沒有被fifinal修飾,或者并非基本類型及字符串,則選擇在類構(gòu)造器中進行初始化。
思考:為什么ConstantValue的屬性值只限于基本類型和string?
因為從常量池中只能引用到基本類型和String類型的字面量
那么這個時候,我們來舉個例子:
假設上面的類變量a被定義為: private static final int a = 1;
編譯時Javac將會為a生成ConstantValue屬性,在準備階段虛擬機就會根據(jù) ConstantValue 的設置將value賦值為1。我們可以理解為static final常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中
解析(Resolve)
把類中的符號引用轉(zhuǎn)換為直接引用
符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用限定符7類符號引用進
行。
直接引用是與虛擬機內(nèi)存布局實現(xiàn)相關,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般
不會相同,如果有了直接引用,那引用的目標必定存在內(nèi)存中。
對解析結(jié)果進行緩存
同一符號引用進行多次解析請求是很常見的,除invokedynamic指令以外,虛擬機實現(xiàn)可以對第一次解析結(jié)果進行緩存,來避免解析動作重復進行。無論是否真正執(zhí)行了多次解析動作,虛擬機需要保證的是在同一個實體中,如果一個引用符號之前已經(jīng)被成功解析過,那么后續(xù)的引用解析請求就應當一直成功;同樣的,如果 第一次解析失敗,那么其他指令對這個符號的解析請求也應該收到相同的異常。
inDy(invokedynamic)是 java 7 引入的一條新的虛擬機指令,這是自 1.0 以來第一次引入新的虛擬機指令。到了 java 8 這條指令才第一次在 java 應用,用在 lambda 表達式中。 indy 與其他 invoke 指令不同的是它允許由應用級的代碼來決定方法解析。這里不演示
初始化(Initialize)
初始化階段是執(zhí)行類構(gòu)造器() 方法的過程。Clint()
或者講得通俗易懂些
在準備階段,類變量已賦過一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源,比如賦值。
在Java中對類變量進行初始值設定有兩種方式:
聲明類變量是指定初始值使用靜態(tài)代碼塊為類變量指定初始值
按照程序員的邏輯,你必須把靜態(tài)變量定義在靜態(tài)代碼塊的前面。因為兩個的執(zhí)行是會根據(jù)代碼編寫的順序來決定的,順序搞錯了可能會影響你的業(yè)務代碼。
JVM初始化步驟:
假如這個類還沒有被加載和連接,則程序先加載并鏈接該類假如該類的直接父類還沒有被初始化,則先初始化其直接父類假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句
使用
那么這個時候我們?nèi)ニ伎家粋€問題,我們的初始化過程什么時候會被觸發(fā)執(zhí)行呢?或者換句話說類初始化時機是什么呢?
主動引用
只有當對類的主動使用的時候才會導致類的初始化,類的主動使用有六種:
創(chuàng)建類的實例,也就是new的方式訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值 或者 調(diào)用類的靜態(tài)方法(如果是final修飾的屬性不行)反射(如 Class.forName(“com.carl.Test”) )初始化某個類的子類,則其父類也會被初始化Java虛擬機啟動時被標明為啟動類的類(JvmCaseApplication ),直接使用 java.exe 命令來運行某個主類
被動引用
引用父類的靜態(tài)字段,只會引起父類的初始化,而不會引起子類的初始化。定義類數(shù)組,不會引起類的初始化。引用類的static final常量,不會引起類的初始化(如果只有static修飾,還是會引起該類初始化的)。
卸載
在類使用完之后,如果滿足下面的情況,類就會被卸載:
該類所有的實例都已經(jīng)被回收,也就是java堆中不存在該類的任何實例。加載該類的ClassLoader已經(jīng)被回收。該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java虛擬機本身會始終引用這些類加載器,而這些類加載器則會始終引用它們所加載的類的Class對象,因此這些Class對象始終是可觸及的。
如果以上三個條件全部滿足,jvm就會在方法區(qū)垃圾回收的時候?qū)︻愡M行卸載,類的卸載過程其實就是在方法區(qū)中清空類信息,java類的整個生命周期就結(jié)束了。但是一般情況下啟動類加載器加載的類不會被卸載,而我們的其他兩種基礎類型的類加載器只有在極少數(shù)情況下才會被卸載。
類加載器ClassLoader
在裝載(Load)階段,其中第(1)步:通過類的全限定名獲取其定義的二進制字節(jié)流,需要借助類裝載器完成,顧名思義,就是用來裝載Class文件的。
什么是類加載器?
負責讀取 Java 字節(jié)代碼,并轉(zhuǎn)換成 java.lang.Class 類的一個實例的代碼模塊。類加載器除了用于加載類外,還可用于確定類在Java虛擬機中的唯一性。
一個類在同一個類加載器中具有唯一性(Uniqueness),而不同類加載器中是允許同名類存在的,這里的同名是指全限定名相同。但是在整個JVM里,縱然全限定名相同,若類加載器不同,則仍然不算作是同一個類,無法通過 instanceOf 、equals 等方式的校驗。
2.2.4.1 分類
1)Bootstrap ClassLoader 負責加載$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath選項指定的jar包。由C++實現(xiàn),不是ClassLoader子類。
2)Extension ClassLoader 負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。
3)App ClassLoader 負責加載classpath中指定的jar包及 Djava.class.path 所指定目錄下的類和jar包。
4)Custom ClassLoader 通過java.lang.ClassLoader的子類自定義加載class,屬于應用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會根據(jù)j2ee規(guī)范自行實現(xiàn)ClassLoader
為什么我們的類加載器要分層?
1.2版本的JVM中,只有一個類加載器,就是現(xiàn)在的“Bootstrap”類加載器。也就是根類加載器。但是這樣會出現(xiàn)一個問題。
假如用戶調(diào)用他編寫的java.lang.String類。理論上該類可以訪問和改變java.lang包下其他類的默認訪問修飾符的屬性和方法的能力。也就是說,我們其他的類使用String時也會調(diào)用這個類,因為只有一個類加載器,我無法判定到底加載哪個。因為Java語言本身并沒有阻止這種行為,所以會出現(xiàn)問題。
這個時候,我們就想到,可不可以使用不同級別的類加載器來對我們的信任級別做一個區(qū)分呢?
比如用三種基礎的類加載器做為我們的三種不同的信任級別。最可信的級別是java核心API類。然后是安裝的拓展類,最后才是在類路徑中的類(屬于你本機的類)。
所以,我們?nèi)N基礎的類加載器由此而生。但是這是我們開發(fā)人員的視角。
public class Demo3 {
public static void main(String[] args) {
// App ClassLoader
System.out.println(new Worker().getClass().getClassLoader());
// Ext ClassLoader
System.out.println(new Worker().getClass().getClassLoader().getParent());
// Bootstrap ClassLoader
System.out.println(new Worker().getClass().getClassLoader().getParent().getParent());
System.out.println(new String().getClass().getClassLoader());
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3a71f4dd
null
null
JVM類加載機制的三種特性
全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
例如,系統(tǒng)類加載器AppClassLoader加載入口類(含有main方法的類)時,會把main方法所依賴的類及引用的類也載入,依此類推?!叭P負責”機制也可稱為當前類加載器負責機制。顯然,入口類所依賴的類及引用的類的當前類加載器就是入口類的類加載器。
以上步驟只是調(diào)用了ClassLoader.loadClass(name)方法,并沒有真正定義類。真正加載class字節(jié)碼文件生成Class對象由“雙親委派”機制完成。
父類委托,“雙親委派”是指子類加載器如果沒有加載過該目標類,就先委托父類加載器加載該目標類,只有在父類加載器找不到字節(jié)碼文件的情況下才從自己的類路徑中查找并裝載目標類。
父類委托別名就叫雙親委派機制。
“雙親委派”機制加載Class的具體過程是:
ClassLoader先判斷該Class是否已加載(緩存機制),如果已加載,則返回Class對象;如果沒有則委托給父類加載器。父類加載器判斷是否加載過該Class,如果已加載,則返回Class對象;如果沒有則委托給祖父類加載器。依此類推,直到始祖類加載器(引用類加載器)。始祖類加載器判斷是否加載過該Class,如果已加載,則返回Class對象;如果沒有則嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入。如果載入成功,則返回Class對象;如果載入失敗,則委托給始祖類加載器的子類加載器。始祖類加載器的子類加載器嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入。如果載入成功,則返回Class對象;如果載入失敗,則委托給始祖類加載器的孫類加載器。依此類推,直到源ClassLoader。源ClassLoader嘗試從其對應的類路徑下尋找class字節(jié)碼文件并載入。如果載入成功,則返回Class對象;如果載入失敗,源ClassLoader不會再委托其子類加載器,而是拋出異常。
從下往上找時,找的是 有無父類加載器;從上往下找時,找的是有無對應的內(nèi)容(class字節(jié)碼文件->類
“雙親委派”機制只是Java推薦的機制,并不是強制的機制。
我們可以繼承java.lang.ClassLoader類,實現(xiàn)自己的類加載器。如果想保持雙親委派模型,就應
該重寫findClass(name)方法;如果想破壞雙親委派模型,可以重寫**loadClass(name)**方法。
緩存機制,緩存機制將會保證所有加載過的Class都將在內(nèi)存中緩存,當程序中需要使用某個Class時,類加載器先從內(nèi)存的緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效.對于一個類加載器實例來說,相同全名的類只加載一次,即 loadClass方法不會被重復調(diào)用。
而這里我們JDK8使用的是直接內(nèi)存,所以我們會用到直接內(nèi)存進行緩存。這也就是我們的類變量為什么只會被初始化一次的由來。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First,在虛擬機內(nèi)存中查找是否已經(jīng)加載過此類...類緩存的主要問題所在?。?!
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();
//調(diào)用此類加載器所實現(xiàn)的findClass方法進行加載
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方法是當字節(jié)碼加載到內(nèi)存后進行鏈接操作,對文件格式和字節(jié)碼驗證,并為 static 字段分配空間并初始化,符號引用轉(zhuǎn)為直接引用,訪問控制,方法覆蓋等
resolveClass(c);
}
return c;
}
}
打破雙親委派
雙親委派這個模型并不是強制模型,而且會帶來一些些的問題。就比如java.sql.Driver這個東西。JDK只能提供一個規(guī)范接口,而不能提供實現(xiàn)。提供實現(xiàn)的是實際的數(shù)據(jù)庫提供商。提供商的庫總不能放JDK目錄里吧。
所以java想到了幾種辦法可以用來打破我們的雙親委派。
SPI(service Provider Interface) :比如Java從1.6搞出了SPI就是為了優(yōu)雅的解決這類問題——JDK提供接口,供應商提供服務。編程人員編碼時面向接口編程,然后JDK能夠自動找到合適的實現(xiàn),豈不是很爽?
Java 在核心類庫中定義了許多接口,并且還給出了針對這些接口的調(diào)用邏輯,然而并未給出實現(xiàn)。開發(fā)者要做的就是定制一個實現(xiàn)類,在 META-INF/services 中注冊實現(xiàn)類信息,以供核心類庫使用。比如JDBC中的DriverManager
OSGI:比如我們的JAVA程序員更加追求程序的動態(tài)性,比如代碼熱部署,代碼熱替換。也就是就是機器
不用重啟,只要部署上就能用。OSGi實現(xiàn)模塊化熱部署的關鍵則是它自定義的類加載器機制的實現(xiàn)。每
一個程序模塊都有一個自己的類加載器,當需要更換一個程序模塊時,就把程序模塊連同類加載器一起
換掉以實現(xiàn)代碼的熱替換。
自定義類加載器
package com.example.jvmcase.loader;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//此方法負責將二進制的字節(jié)碼轉(zhuǎn)換為Class對象
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
String fileName = root +
File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
// classLoader.setRoot("D:\\codes\\jvm-case\\src\\main\\java");
classLoader.setRoot("D:\\");
Class> testClass = null;
try {
// testClass = classLoader.loadClass("com.example.jvmcase.basic.Test");
testClass = classLoader.loadClass("Test");
System.out.println(testClass);
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
自定義類加載器的核心在于對字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在該類中對文件進行解密。由于這里只是演示,我并未對class文件進行加密,因此沒有解密的過程。這里有幾點需要注意:
1、這里傳遞的文件名需要是類的全限定性名稱,即 Test 格式的,因為 defifineClass 方法是按這種格式進行處理的。
如果沒有全限定名,那么我們需要做的事情就是將類的全路徑加載進去,而我們的setRoot就是前綴地址 setRoot + loadClass的路徑就是文件的絕對路徑
2、最好不要重寫loadClass方法,因為這樣容易破壞雙親委托模式。3、這類Test 類本身可以被 AppClassLoader 類加載,因此我們不能把 Test.class 放在類路徑下。否則,由于雙親委托機制的存在,會直接導致該類由 AppClassLoader 加載,而不會通過我們自定義類加載器來加載。
如果我們把Test放在類路徑之下,那么我們將會通過AppClassLoader加載
打印結(jié)果:
class com.example.jvmcase.basic.Test
sun.misc.Launcher$AppClassLoader@18b4aac2
柚子快報邀請碼778899分享:JVM類加載
精彩鏈接
本文內(nèi)容根據(jù)網(wǎng)絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。