
加載
當我們要使用一個類的時候,要通過ClassLoader將類加載到記憶體中
類加載階段主要完成如下三件事情
- 通過全類名,獲取類的二進制流
- 決議類的二進制流為方法區內的資料結構
- 創建一個java.lang.Class類的實體,表示該型別,作為方法區這個類的訪問入口

通過全類名,獲取類的二進制流的方式有很多種
- 從zip壓縮包中獲取
- 從網路中獲取
- 運行時計算生成,如動態代理技術
- …
對于非陣列型別的加載階段,即可以使用Java虛擬機內置的類加載器去完成,也可以使用用戶自定義的類加載器去完成
鏈接
鏈接這個階段主要分為3個部分,驗證,準備,決議
驗證
驗證階段主要是確保Class檔案的格式正確,運行時不會危害虛擬機的安全
驗證階段的規則很多,但大致分為如下4個階段

具體詳細的內容,我就不詳細解釋了,可以看《深入理解Java虛擬機》,本篇文章偏向于做一個總結,把握類加載的一個整體流程,而不對細節進行闡述
準備
準備階段主要是為類的靜態變數分配記憶體,并將其初始化為默認值
常見的資料型別的默認值如下
| 資料型別 | 默認值 |
|---|---|
| byte | (byte)0 |
| short | (short)0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| boolean | false |
| char | ‘\u0000’ |
| reference | null |
如果類靜態變數的欄位屬性表中存在ConstantValue屬性,則直接執行賦值陳述句
那么什么情況下類靜態變數的欄位屬性表中存在ConstantValue屬性呢?
- 類靜態變數為基本資料型別,并且被final修飾
- 類靜態變數為String型別,被final修飾,并且以字面量的形式賦值
為了方便查看Class檔案的位元組碼,我在IDEA中下載了一個插件jclasslib Bytecode viewer,非常方便,用如下代碼通過位元組碼的形式驗證一下
public class Person {
private static int age = 10;
private static final int length = 160;
private static final String name = "name";
private static final String loc = new String("loc");
}

所以length和name屬性在準備階段就會賦值為ConstantValue指定的值
那么age和loc屬性會在哪個階段賦值呢?是在初始化階段,后面會詳細介紹哈

決議
將類,介面,欄位和方法的符號參考(在常量池中)轉為直接參考
符號參考:用一組符號來描述所參考的目標
直接參考;直接指向指向目標的指標
加入我寫了一個如下的類
public class Student {
private String name;
private int age;
public String getName() {
return this.name;
}
}
以欄位為例,name和age對應的物件并不是直接指向記憶體地址,而是用字串來進行描述(即符號參考),決議階段就是將這些描述轉為直接指向目標的指標(即直接參考)
初始化
執行類靜態成員變數賦值陳述句和靜態代碼塊中的陳述句

我們把上面的Student代碼改成如下形式
public class Student {
private String name;
private int age = 10;
private static int gender = 1;
{
System.out.println("構造代碼塊");
}
static {
System.out.println("靜態代碼塊");
}
public Student() {
System.out.println("建構式");
}
public String getName() {
return this.name;
}
}
可以看到位元組碼中包含了3個方法,getName方法我們知道,<init>和<clinit>方法里面執行了哪些邏輯?

從位元組碼的角度分析一波
<init>方法

從位元組碼可以看到<init>方法的主要邏輯為
- 呼叫父類的<init>方法
- 非靜態成員變數賦值
- 執行構造代碼塊
- 執行建構式

<clinit>方法

從位元組碼可以看到<clinit>方法的主要邏輯為
- 執行靜態變數的賦值陳述句
- 執行靜態代碼塊中的陳述句
- 需要注意的一點是,Java虛擬機會保證子類的<client>方法執行前,父類的<client>方法已經執行完畢
理解<clinit>和<init>方法的作用還是很有必要的,因為經常有些面試題問靜態代碼塊,構造代碼塊,建構式的執行順序,
我這里就直接總結一下結論,大家可以寫demo驗證一下
沒有繼承情況的執行順序
- 靜態代碼塊和靜態成員變數,執行順序由撰寫順序決定(只會執行一次哈)
- 構造代碼塊和非靜態成員變數,執行順序由撰寫順序決定
- 建構式
有繼承情況的執行順序
- 父類的靜態(靜態代碼塊,靜態成員變數),子類的靜態(靜態代碼塊,靜態成員變數)(只會執行一次哈)
- 父類的非靜態(構造代碼塊,非靜態成員變數),父類的建構式
- 子類的非靜態(構造代碼塊,非靜態成員變數),子類的建構式
卸載
垃圾收集不僅發生在堆中,方法區上也會發生,但是對方法區的型別資料回收的條件比較苛刻


以下圖為例,想回收方法區中的Simple類
- 需要保證堆中的Sample類及其子類都已經被回收
- 加載Sample類的MyClassLoader已經被回收
- Sample類對應的Class物件已經被回收

可以看到對方法區的型別資料回收的條件比較苛刻,但是收效甚微,所以有些垃圾收集器不會對方法區的型別資料進行回收
總結
類加載程序

變數的賦值程序

參考博客
[1]https://www.cnblogs.com/caoxb/p/12735525.html
[2]https://m.xp.cn/b.php/103736.html
好文
[3]https://ricstudio.top/archives/classloadmechanism
<init>和<client>方法的執行時機
[4]https://www.zhihu.com/question/447387629/answer/1762489678
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/289859.html
標籤:java
