一、基礎架構
概覽
我們平時說的棧是指的Java棧,native method stack 裡面裝的都是native方法
細節架構圖
二、類載入器
1、類的載入
- 方法區並不是存放方法的區域,其是存放類的描述資訊(模板)的地方
- Class loader只是負責class檔案的載入,相當於快遞員,這個“快遞員”並不是只有一家,Class loader有多種
- 載入之前是“class”,載入之後就變成了“Class”,這是安裝java.lang.Class模板生成了一個例項。“Class”就裝載在方法區,模板例項化之後就得到n個相同的物件
- JVM並不是通過檢查檔案字尾是不是
.class
來判斷是否需要載入的,而是通過檔案開頭的特定檔案標誌
2、類的載入過程
注意:載入階段失敗會直接丟擲異常
2.1、載入
把.class檔案讀入到java虛擬機器中
- 通過“類全名”來獲取定義此類的二進位制位元組流
動態編譯:jsp-->java-->class
-
將位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構
-
在java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口
2.2、連結
1. 驗證
-
確保class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身安全。
-
驗證階段主要包括四個檢驗過程:檔案格式驗證、後設資料驗證、位元組碼驗證和符號引用驗證。
2. 準備
- 為類變數(靜態變數)分配記憶體並設定類變數預設值-->0/false/null(不包含final修飾的static,final修飾的變數會顯示初始化)
- 在初始化之前,若使用了類變數,用到的是預設值,並非程式碼中賦值的值
- 不會為例項變數分配初始化、類變數分配在方法區中,例項變數會隨物件分配到java堆中
3. 解析
虛擬機器常量池內的符號引用替換為直接引用 ,類名、欄位名、方法名--->具體記憶體地址或偏移量
2.3、初始化
1. 主動/被動使用
2. 初始化注意點
-
類變數被賦值、例項變數被初始化
-
每個類/介面被Java程式首次主動使用的時候才會被java虛擬機器初始化
-
從上到下初始化、
-
初始化一個類時,要求它的父類都已經被初始化了(除介面)
-
當初始化一個類的時候並不會先初始化它實現的介面
-
當初始化一個介面的時候,並不會初始化它的父介面
一個父介面並不會因為它的子介面或實現類的初始化而初始化,只有當首次使用其特定的靜態變數時(即執行時常量,如介面中引用型別的變數)時才會初始化
-
3. 深入理解舉例1
- 對於靜態欄位來說,只有直接定義了該欄位的類才會被初始化
- 每個類在初始化前,必須先初始化其父類(除介面)
- 追蹤類的載入情況:-XX:+TraceClassLoading(+表示開啟,-表示關閉)
- 對於常量(這裡指編譯器確定的常量)來說,常量值在編譯階段會存入到呼叫它的方法所在的類的常量池中,本質上呼叫類沒有直接引用到定義常量的類
- 對於引用型別陣列來說,其型別是由JVM在執行期間動態生成的,表示為
[L+自定義類全類名
(一維)這種形式 - 準備階段只是分配記憶體、賦預設值,初始化階段才是真正的賦值(自己設定的值)
- 初始化階段是從上到小初始化賦值
public class ClassLoaderTest {
public static void main(String[] args) {
//單獨測試下列語句
//1.
System.out.println(Child.str1);
/*輸出
* Parent static block
* hello I'm Parent
*/
//2.
System.out.println(Child.str2);
/*輸出
* Parent static block
* Child static block
* hello I'm Child
*/
//3.
System.out.println(Parent.str3);
/*輸出
* hello I'm Parent2
* */
//4.
System.out.println(Parent.str4);
/*輸出
* Parent static block
* 78f59c0d-b91c-4e32-8109-dec5cb23aa13
* */
//5.
Parent[] parents1=new Parent[1];
System.out.println(parents1.getClass());
Parent[][] parents2=new Parent[2][2];
System.out.println(parents2.getClass());
/*輸出
* class [Lcom.lx.Parent;
* class [[Lcom.lx.Parent;
* */
//6.
System.out.println(Singleton1.count1);
System.out.println(Singleton1.count2);
System.out.println(Singleton2.count1);
System.out.println(Singleton2.count2);
/*輸出
* 1,1,1,0
* */
}
}
class Parent{
public static String str1 = "hello I'm Parent";
public static final String str3 = "hello I'm Parent2";
public static final String str4 = UUID.randomUUID().toString();
static {
System.out.println("Parent static block");
}
}
class Child extends Parent{
public static String str2 = "hello I'm Child";
static {
System.out.println("Child static block");
}
}
class Singleton1 {
public static int count1;
public static int count2=0;
public static Singleton1 singleton1=new Singleton1();
public Singleton1() {
count1++;
count2++;
}
public Singleton1 getInstance(){
return singleton1 ;
}
}
class Singleton2 {
public static int count1;
public static Singleton2 singleton2=new Singleton2();
public Singleton2() {
count1++;
count2++;
}
public static int count2=0;
public Singleton2 getInstance(){
return singleton2 ;
}
}
4. 結果分析
- Child屬於被動使用,Parent是主動使用,所以只會初始化Parent
- Child屬於主動使用,所以會初始化Child,由於初始化的類具有父類所以先初始化父類
- Parent並沒有被使用到,str3的值在編譯期間就被存入CLassLoaderTest這個呼叫它的方法所在的類的常量池中,與Parent無關
- str4不是編譯期間就能確定的常量,就不會放到呼叫方法類的常量池中,在執行時主動使用Parent類進而需要初始化該類
- 沒有對Parent類初始化,引用陣列型別並非Parent類,而是jvm動態生成的class [Lcom.lx.Parent
- 首先訪問Singleton的靜態方法--》Singleton是主動使用--》先初始化
- 第一種:準備階段給count1,2分配空間預設值已經為0了,此時給類變數singleton初始化,呼叫構造方法,分別加一
- 第二種:同上,但是在給singleton初始化時,count2並未初始化,自增只是暫時的,隨後就要對它初始化,所以在count2初始化前對他進行的操作時無效的。
類載入情況
情況1:
- 載入object....類
- 載入啟動類
- 載入父類
- 載入子類
類的載入並非一定要該類被主動使用化
情況2:同上
情況3:
自定義的類只載入了啟動類(呼叫常量的方法所在的類)
情況4:載入啟動類以及Parent類
反編譯結果
情況1:
情況2:類似1
情況3:沒有引用到Parent類(定義常量的類)
情況4:類似1
5. 深入理解舉例2
介面中定義的變數都是常量
常量又分為編譯期常量和執行期常量,編譯期常量的值在編譯期間就可以確定,直接儲存在了呼叫類的常量池中,所以訪問介面中的編譯期常量並不會導致介面的初始化,只有訪問介面中的執行期常量才會引起介面的初始化。
父介面並不會因為子介面或是實現類的初始化而初始化,當訪問到了其特定的靜態變數時(即執行時常量,如介面中引用型別的變數)才會初始化
public class ClassLoaderTest2 {
public static void main(String[] args) {
System.out.println(new demo2().a);
System.out.println("=====");
System.out.println(son1.a);
new demo1().show();
System.out.println(demo1.str);
System.out.println(son1.b);
System.out.println(demo1.s);//System.out.println(son1.s);
/*輸出
* father2 singleton
* 1
* =====
* 1
* show method
* string
* father1 singleton
* com.lx.father1$1@1b6d3586
* */
}
}
interface father1{
int a=1;
void show();
String str="string";
Singleton1 s=new Singleton1(){
{
System.out.println("father1 singleton");
}
};
}
interface son1 extends father1 {
int b=0;
Singleton1 s1=new Singleton1(){
{
System.out.println("son1 singleton");
}
};
}
class demo1 implements father1{
@Override
public void show() {
System.out.println("show method");
}
}
class father2{
int a=1;
void show(){}
String str="string";
Singleton1 s=new Singleton1(){
{
System.out.println("father2 singleton");
}
};
}
class demo2 extends father2{
}
6. 結果分析
第3行:子類初始化前必須初始化父類
第5-8行:訪問到編譯時常量(已經存入了呼叫方法類的常量池中),不會導致初始化
第9行: 訪問了執行時常量,需要初始化定義該執行時常量的類
3、類載入器分類
一、java虛擬機器自帶的類載入器
-
啟動類載入器(Bootstrap) ,C++所寫,不是ClassLoader子類
-
擴充套件類載入器(Extension) ,Java所寫
-
應用程式類載入器(AppClassLoader)。
- 自定義類一般為系統(應用)類載入器載入
二、使用者自定義的類載入器
import com.gmail.fxding2019.T;
public class Test{
//Test:檢視類載入器
public static void main(String[] args) {
Object object = new Object();
//檢視是那個“ClassLoader”(快遞員把Object載入進來的)
System.out.println(object.getClass().getClassLoader());
//檢視Object的載入器的上一層
// error Exception in thread "main" java.lang.NullPointerException(已經是祖先了)
//System.out.println(object.getClass().getClassLoader().getParent());
System.out.println();
Test t = new Test();
System.out.println(t.getClass().getClassLoader().getParent().getParent());
System.out.println(t.getClass().getClassLoader().getParent());
System.out.println(t.getClass().getClassLoader());
}
}
/*
*output:
* null
*
* null
* sun.misc.Launcher$ExtClassLoader@4554617c
* sun.misc.Launcher$AppClassLoader@18b4aac2
* */
- 如果是JDK自帶的類(Object、String、ArrayList等),其使用的載入器是Bootstrap載入器;如果自己寫的類,使用的是AppClassLoader載入器;Extension載入器是負責將把java更新的程式包的類載入進行
- 輸出中,sun.misc.Launcher是JVM相關呼叫的入口程式
- Java載入器個數為3+1。前三個是系統自帶的,使用者可以定製類的載入方式,通過繼承Java. lang. ClassLoader
4、雙親委派機制
Java虛擬機器採用按需載入的方式,當需要使用該類是才會去講class檔案載入到記憶體生成class物件,載入類是採用的是雙親委派機制
自底向上檢查類是否已經被載入
自頂向下嘗試載入類
原理圖:
另外一種機制:
雙親委派優勢:
- 避免類的重複載入。
- 保護程式安全、防止核心api被惡意篡改(如下例子)
使用者自定義的類載入器不可能載入到一個有父載入器載入的可靠類,從而防止不可靠惡意程式碼代替父載入器載入的可靠的程式碼。例如:Object類總是有跟類載入器載入,其他使用者自定義的類載入器都不可能載入含有惡意程式碼的Object類
//測試載入器的載入順序
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
/*
* output:
* 錯誤: 在類 java.lang.String 中找不到 main 方法
* */
解釋:
交給啟動類載入器之後(java.lang.String/由java開頭的包名)歸它管,所以它首先載入這個類(如果核心api內沒有改類也會報錯),輪不到讓系統類載入器去載入該類,即無法載入到自己所寫的String類,核心api中的String類沒有main方法,所以會報錯說找不到main方法
5、補充:
類的例項化
- 為新的物件分配記憶體
- 為例項變數賦預設值
- 為例項變數賦值(自己定義的)
- 為其生成
/ 方法或者說構造方法
判斷為同一個類的必要條件
使用類載入器的原因
自定義類載入器
獲取類載入器方法:
沙箱安全機制
名稱空間
loadClass方法
通過呼叫ClassLoader類的loadClass方法載入一個類,並不是對一個類的主動使用,不會導致初始化。
類的解除安裝