一、基礎架構 概覽 我們平時說的棧是指的Java棧,native method stack 裡面裝的都是native方法 細節架構圖 二、類載入器 1、類的載入 方法區並不是存放方法的區域,其是存放類的描述信息(==模板==)的地方 Class loader只是負責class文件的載入,相當於快遞員 ...
一、基礎架構
概覽
我們平時說的棧是指的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方法載入一個類,並不是對一個類的主動使用,不會導致初始化。
類的卸載