類加載的5個過程
類加載的本質(zhì)
將描述類的數(shù)據(jù) 從Class文件加載到內(nèi)存并且對數(shù)據(jù)進行校驗 轉(zhuǎn)換解析和初始化 最終新城虛擬機直接使用java使用類型
類加載過程
-
加載
-
作用
將外部的Class文件加載到虛擬機并且存儲到方法區(qū)內(nèi)
-
具體流程
通過類名的全限定名來獲取定義此類的二進制數(shù)據(jù)
將這個字節(jié)流所代表的的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生成一個代表這個類的java.lang.class對象 作為方法區(qū)該類的各種數(shù)據(jù)的訪問入口
-
注意
- 數(shù)組類是通過java虛擬機直接創(chuàng)建 不通過類的加載機制
-
-
驗證
-
作用 確
保加載進來的class文件包含的信息符合jvm的要求
-
具體流程
文件格式的校驗
元數(shù)據(jù)校驗
字節(jié)碼校驗
符號引用校驗
-
-
準(zhǔn)備
-
作用
為類變量分配內(nèi)存 并且設(shè)置類變量的初始值
-
具體流程
為類的static變量在方法區(qū)中分配內(nèi)存
將上述變量的初始值設(shè)置為0
-
注意
實例變量不在該階段分配內(nèi)存
若該類為常量(final修飾) 直接復(fù)制開發(fā)者定義的值
-
-
解析
-
作用
將常量池內(nèi)的符號引用轉(zhuǎn)為直接引用
-
具體流程
解析對象(類/接口) 方法 (類方法 接口方法 方法類型 方法句柄) 字段
-
注意
實例變量不在該階段分配內(nèi)存
因為類方法和私有方法符合 "編譯器可知 , 運行期不可變" 的要求 即不會被繼承或者重寫 所以適合類加載過程進行解析
若類變量為常量 (final 修飾 ) 則直接賦值開發(fā)者定義的值
-
-
初始化
-
作用
初始化類變量 靜態(tài)語句塊
-
具體流程
生成類構(gòu)造器 clinit() 即合并所有類變量和靜態(tài)語句塊
執(zhí)行clinit()方法
-
注意
-
類構(gòu)造器clinit區(qū)別于類構(gòu)造器 init
不需要調(diào)用父類構(gòu)造器
子類clinit執(zhí)行前 父類的clinit一定會被執(zhí)行
虛擬機第一個執(zhí)行的clinit是 java.lang.object
靜態(tài)語句塊只可被賦值不能被訪問
接口與類不同 執(zhí)行子接口的clinit并不需要執(zhí)行負(fù)借口的clinit
-
-
對象的創(chuàng)建 內(nèi)存分配 訪問定位
對象的創(chuàng)建
A a =new A(); //當(dāng)遇到關(guān)鍵字new指令時,Java對象創(chuàng)建過程便開始</pre>
加載過程
類加載檢查
檢查該new指令的參數(shù) 是否在常量池中定位到了一個類的符號引用 沒有即創(chuàng)建對象失敗
檢查該類符號引用代表的類是否已經(jīng)被加載,解析和初始化過
如果沒有 需要先執(zhí)行類的加載過程
為對象分配內(nèi)存
對象所需要的內(nèi)存大小在類加載完成后便可以完全確定
內(nèi)存分配 根據(jù)java堆內(nèi)存是否絕對規(guī)整分為
-
指針碰撞 Compat 收集器
假設(shè)java堆內(nèi)存絕對規(guī)整 內(nèi)存分配采用指針碰撞
分配形式: 已使用內(nèi)存在一邊 未使用的在另一邊 中間放一個座位分界點的指示器
那么 分配對象內(nèi)存 = 指針針向未使用內(nèi)存一定一段與對象大小相等的距離
-
空閑列表 CMS 收集器
假設(shè)java堆內(nèi)存不規(guī)整 內(nèi)存分配將采用空閑列表
分配形式 :虛擬機維護著一個記錄可用內(nèi)存塊的列表 在分配時從列表中找到一塊足夠大的空間劃分給對象實例 ,并更新列表上的記錄
內(nèi)存創(chuàng)建在虛擬機中非常常見 存在并發(fā)情況下也會引起線程不安全
解決辦法
同步處理分配內(nèi)存空間 虛擬機采用CAS + 失敗重試 保證更新操作的原子性
-
把內(nèi)存分配行為按照線程劃分在不同的內(nèi)存空間進行
即每個線程在
Java堆中預(yù)先分配一小塊內(nèi)存(本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)),哪個線程要分配內(nèi)存,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時才需要同步鎖。虛擬機是否使用
TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定。
將內(nèi)存空間初始化為0
內(nèi)存分配完成后 虛擬機需要將分配的內(nèi)存空間初始化為0
- 保證對象的實例字段在使用時不可復(fù)制就能直接適應(yīng) (默認(rèn)為0)
- 如果使用本線程分配緩沖(TLAB) 這一工作過程可以提前至TLAB分配時進行
對對象進行必要的設(shè)置
設(shè)置對象是哪個類的實例 如何找到類的源數(shù)據(jù) 對象的哈希嗎 對象的GC分代信息等
這個信息存放在對象的對象頭中
對象的內(nèi)存分配
在java對象創(chuàng)建后 打底是如何被存儲在java內(nèi)存中的呢
在java虛擬機中 對象內(nèi)存中 存儲布局可以分為三塊
1. 對象頭
1.1 對象自身的運行時數(shù)據(jù) ( Mark Word )
如Hash 碼 GC分代 鎖狀態(tài) 線程持有的鎖 偏向線程 id 偏向時間戳
該部分?jǐn)?shù)據(jù)設(shè)計成1個非固定的數(shù)據(jù)結(jié)構(gòu) 一邊在績效的空間存儲更多信息
1.2 對象類型指針
對象指向它的類元數(shù)據(jù)的指針
虛擬機通過這個指針確定這個對象是哪個類的實例
1.3 數(shù)組的對象頭
如果是對象是數(shù)組 name在對象頭中還有一塊記錄數(shù)組長度的數(shù)據(jù)
因為虛擬機可以通過普通java 對象的元數(shù)據(jù)確定對象的大小 但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小
2. 實例數(shù)據(jù)
存儲的信息 對象真正有效的信息
代碼中定義的字段內(nèi)容
- 這部分?jǐn)?shù)據(jù)的存儲順序會受到虛擬機分配參數(shù)(FieldAllocationStyle)和字段在Java源碼中定義順序的影響。
3. 對齊填充 (非必須)
存儲 占位符
占位作用
因為對象的大小必須是8字節(jié)的整數(shù)倍(即對象的大小不是8字節(jié)的整數(shù)倍),就需要通過對齊填充來補全。
對象的訪問定位
對象建立后 如何去訪問對象?
實際上 需要訪問的是 對象類型數(shù)據(jù) 和 對象實例數(shù)據(jù)
java 程序 通過 棧上的引用類型(reference) 來訪問java棧上的對象
句柄訪問
直接指針訪問
JVM 分派
分派: 確定執(zhí)行那個方法的過程
靜態(tài)分派
1.1 定義
根據(jù)變量的靜態(tài)類型進行方法分派的行為 根據(jù)變量的靜態(tài)類型 確定執(zhí)行那個方法 發(fā)生在編譯器 不由JVM來執(zhí)行
public class Test {
// 類定義
static abstract class Human {
}
// 繼承自抽象類Human
static class Man extends Human {
}
static class Woman extends Human {
}
// 可供重載的方法
public void sayHello(Human guy) {
System.out.println("hello,Human!");
}
public void sayHello(Man guy) {
System.out.println("hello Man!");
}
public void sayHello(Woman guy) {
System.out.println("hello Woman!");
}
// 測試代碼
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
Test test = new Test();
test.sayHello(man);
test.sayHello(woman);
}
}
// 運行結(jié)果
hello,Human!
hello,Human!
1.2 方法重載
重載=靜態(tài)分派 =根據(jù)變量的靜態(tài)類型確定執(zhí)行那個重載方法
1.3 變量的靜態(tài)類型發(fā)生變化
強制裝換類型 改變變量的靜態(tài)類型
Human man = new Man();
test.sayHello((Man)man);
1.4 靜態(tài)分配的優(yōu)先級匹配
靜態(tài)分派優(yōu)先選擇參數(shù)類型一致的重載方法
沒有最合適的方式 進行重載時 會選擇(第二優(yōu)先級)的方法重載 基本數(shù)據(jù)類型優(yōu)先級分配
第二優(yōu)先級順序 : char > int > long > float > double > character > serializable> object .. args..
最后的args 為可變長參數(shù) 可以理解為數(shù)組
因為char轉(zhuǎn)為byte 或者short過程不安全 所以不會選擇參數(shù)類型為byte或者short進行重載
- 引用類型分配 根據(jù)繼承關(guān)系進行優(yōu)先級匹配
動態(tài)分派
2.1 定義
根據(jù)變量的動態(tài)類型確定執(zhí)行那個方法
// 定義類
class Human {
public void sayHello(){
System.out.println("Human say hello");
}
}
// 繼承自 抽象類Human 并 重寫sayHello()
class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
// 測試代碼
public static void main(String[] args) {
// 情況1
Human man = new man();
man.sayHello();
// 情況2
man = new Woman();
man.sayHello();
}
}
// 運行結(jié)果
man say hello
woman say hello
// 原因解析
// 1. 方法重寫(Override) = 動態(tài)分派 = 根據(jù) 變量的動態(tài)類型 確定執(zhí)行(重寫)哪個方法
// 2. 對于情況1:根據(jù)變量(Man)的動態(tài)類型(man)確定調(diào)用man中的重寫方法sayHello()
// 3. 對于情況2:根據(jù)變量(Man)的動態(tài)類型(woman)確定調(diào)用woman中的重寫方法sayHello()