[TOC]
虛擬機把描述類的數據從Class文件**加載**到內存,并對數據進行**校驗**、轉換**解析**和**初始化**,最終成為被虛擬機直接使用的Java對象,這就是JVM的類加載機制。
??? Java天生的可動態擴展的語言特性就是依賴運行期的**動態加載**和**動態連接**實現的。
</br>
## ??? 一:類的生命周期
??? 類的生命周期包括7個部分:加載——驗證——準備——解析——初始化——使用——卸載
??? 其中,驗證——準備——解析? 稱為連接階段。除了解析外,其他階段是順序發生的,而解析可以與這些階段交叉進行,因為Java支持動態綁定(晚期綁定),需要運行時才能確定具體類型。
## ??? 二:類的初始化觸發
??? 類的加載機制沒有明確的觸發條件,但是有5種情況下必須對類進行初始化,那么 加載——驗證——準備 就必須在此之前完成了。
??? 1:new、getstatic、putstatic、invokestatic這4個? 字節碼指令? 時對類進行初始化(即:**實例化對象、讀寫靜態對象、調用靜態方法時,進行類的初始化**);
??? 2:使用反射機制對類進行調用時,進行類的初始化;
??? 3:初始化一個類,其父類沒有初始化時,先初始化其父類;
??? 4:虛擬機啟動時,初始化一個執行主類;
??? 5:使用JDK1.7的**動態語言**支持時,如果MethodHandle實例的解析結果為REF\_getstatic、REF\_putstatic、REF\_invokestatic的方法句柄(即:讀寫靜態對象或者調用靜態方法),則初始化該句柄對應類;
??? 一般,以上5種情況**最常見的是前三種:實例化對象、讀寫靜態對象、調用靜態方法、反射機制調用類、調用子類觸發父類初始化**。
## ??? 三:類的加載過程
??? 從用戶角度來說,類(對象)的生命周期只需籠統理解為“加載——使用——卸載”即可,無需太過深入。所以,這里的類加載過程就是我們說的 加載——驗證——準備——解析(非必須)——初始化? 這五個使用前的階段。
### ??? 1:加載
?????? 加載階段,虛擬機需要完成三件事:**通過類名字獲取類的二進制字節流——將字節流的內容轉存到方法區——在內存中生成一個Class對象作為該類方法區數據的訪問入口**。
?????? 其中,第一步:通過類名獲取類的二進制字節流是通過類加載器來完成的。其加載過程使用“雙親委派模型”:
?????? 類加載器的層次結構為:

?????? 啟動類加載器:加載系統環境變量下JAVA\_HOME/lib目錄下的類庫。
?????? 擴展類加載器:加載JAVA\_HOME/lib/ext目錄下的類庫。
?????? 應用程序類加載器(系統類加載器):加載用戶類路徑Class\_Path指定的類庫。(我們可以在使用第三方插件時,把jar包添加到ClassPath后就是使用了這個加載器)
?????? 自定義加載器:如果需要自定義加載時的規則(比如:指定類的字節流來源、動態加載時性能優化等),可以自己實現類加載器。
?????? 雙親委派模型是指:當一個類加載器收到類加載請求時,不會直接加載這個類,而是把這個加載請求委派給自己父加載器去完成。如果父加載器無法加載時,子加載器才會去嘗試加載。
?????? 采用雙親委派模型的原因:避免同一個類被多個類加載器重復加載。
### ??? 2:驗證
?????? 確保class文件的二進制字節流中包含的信息符號虛擬機要求,包括:文件格式驗證、元數據驗證(數據語義分析)、字節碼驗證(數據流語義合法性)、符號引用驗證(符號引用的匹配性校驗,確保解析能正確執行)
### ??? 3:準備
?????? 為**類變量(靜態變量)**在**方法區**分配內存,并設置**零值**。注意:這里是類變量,不是實例變量,實例變量是對象分配到**堆內存**時根據運行時動態生成的。
### ??? 4:解析
?????? 把常量池中的符號引用解析為直接引用:根據符號引用所作的描述,在內存中找到符合描述的目標并把目標指針指針返回。
---
關于符號引用與直接引用,我們還是用一個實例來分析吧。看下面的 Java 代碼:
```
package test;
public class Test {
public static void main(String[] args) {
Sub sub = new Sub();
int a = 100;
int d = sub.inc(a);
}
}
class Sub {
public int inc(int a) {
return a + 2;
}
}
```
編譯后使用 javap 分析工具,會得到下面的 Class 文件內容:
```
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = Class #16 // test/Sub
#3 = Methodref #2.#15 // test/Sub."":()V
#4 = Methodref #2.#17 // test/Sub.inc:(I)I
#5 = Class #18 // test/Test
#6 = Class #19 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 (\[Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "":()V
#16 = Utf8 test/Sub
#17 = NameAndType #20:#21 // inc:(I)I
#18 = Utf8 test/Test
#19 = Utf8 java/lang/Object
#20 = Utf8 inc
#21 = Utf8 (I)I
{
public test.Test();
descriptor: ()V
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
Code:
stack=2, locals=4, args_size=1
0: new #2 // class test/Sub
3: dup
4: invokespecial #3 // Method test/Sub."<init>":()V
7: astore_1
8: bipush 100
10: istore_2
11: aload_1
12: iload_2
13: invokevirtual #4 // Method test/Sub.inc:(I)I
16: istore_3
17: return
}
```
因為篇幅有限,上面的內容只保留了常量池,和 Code 部分。下面我們主要對 inc 方法的調用來進行說明。
**符號引用
**
在 main 方法的字節碼中,調用 inc 方法的指令如下:
```
13: invokevirtual #4 // Method test/Sub.inc:(I)I
```
invokevirtual 指令就是調用實例方法的指令,后面的操作數 4 是 Class 文件中常量池的下標,表示用來指定要調用的目標方法。我們再來看常量池在這個位置上的內容:
```
#4 = Methodref #2.#17
```
這是一個 Methodref 類型的數據,我們再來看看虛擬機規范中對該類型的說明:
```
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
```
這實際上就是一種引用類型,tag 表示了常量池數據類型,這里固定是 10。class_index 表示了類的索引,name_and_type_index 表示了名稱與類型的索引,這兩個也都是常量池的下標。在 javap 的輸出中,已經將對應的關系打印了出來,我們可以直接的觀察到它都引用了哪些類型:
```
#4 = Methodref #2.#17 // test/Sub.inc:(I)I
|--#2 = Class #16 // test/Sub
| |--#16 = Utf8 test/Sub
|--#17 = NameAndType #20:#21 // inc:(I)I
| |--#20 = Utf8 inc
| |--#21 = Utf8 (I)I
```
這里我們將其表現為樹的形式。可以看到,我們可以得到該方法所在的類,以及方法的名稱和描述符。于是我們根據 invokevirtual 的操作數,找到了常量池中方法對應的 Methodref,進而找到了方法所在的類以及方法的名稱和描述符,當然這些內容最終都是字符串形式。
實際上這就是一個符號引用的例子,符號引用也可以理解為像這樣使用文字形式來描述引用關系。
**直接引用
**
符號引用在上面說完了,我們知道符號引用大概就是文字形式表示的引用關系。但是在方法的執行中,只有這樣一串字符串,有什么用呢?方法的本體在哪里?下面這就是直接引用的概念了,這里我用自己目前的理解總結一下,直接引用就是通過對符號引用進行解析,來獲得真正的函數入口地址,也就是在運行的內存區域找到該方法字節碼的起始位置,從而真正的調用方法。
那么將符號引用解析為直接引用的過程是什么樣的呢?我這個小渣渣目前也給不出確定的答案,在 JVM里的符號引用如何存儲? 里,RednaxelaFX 大大給出了一個 Sun JDK 1.0.2 的實現;在 自己動手寫Java虛擬機 中,作者給出了一種用 Go 的簡單實現,下面這里就來看一下這個簡單一些的實現。在 HotSpot VM 中的實現肯定要復雜得多,這里還是以大致的學習了解為主,以后如果有時間有精力,再去研究一下 OpenJDK 中 HotSpot VM 的實現。
不過不管是哪種實現,肯定要先讀取 Class 文件,然后將其以某種格式保存在內存中,類的數據會記錄在某個結構體內,方法的數據也會記錄在另外的結構體中,然后將結構體之間相互組合、關聯起來。比如,我們用下面的形式來表達 Class 的數據在內存中的保存形式:
```
type Class struct {
accessFlags uint16 // 訪問控制
name string // 類名
superClassName string // 父類名
interfaceNames []string // 接口名列表
constantPool *ConstantPool // 該類對應的常量池
fields []*Field // 字段列表
methods []*Method // 方法列表
loader *ClassLoader // 加載該類的類加載器
superClass *Class // 父類結構體的引用
interfaces []*Class // 各個接口結構體的引用
instanceSlotCount uint // 類中的實例變量數量
staticSlotCount uint // 類中的靜態變量數量
staticVars Slots // 類中的靜態變量的引用列表
initStarted bool // 類是否被初始化
}
```
類似的,常量池中的方法引用,也要有類似的結構來表示:
```
type MethodRef struct {
cp *ConstantPool // 常量池
className string // 所在的類名
class *Class // 所在的類的結構體引用
name string // 方法名
descriptor string // 描述符
method *Method // 方法數據的引用
}
```
回到上面符號解析的例子。當遇到 invokevirtual 指令時,根據后面的操作數,可以去常量池中指定位置取到方法引用的結構體。實際上這個結構體中已經包含了上面看到的各種符號引用,最下面的 method 就是真正的方法數據。類加載到內存中時,method 的值為空,當方法第一次調用時,會根據符號引用,找到方法的直接引用,并將值賦予 method。從而后面再次調用該方法時,只需要返回 method 即可。下面我們看方法的解析過程:
```
func (self *MethodRef) resolveMethodRef() {
c := self.ResolvedClass()
method := lookupMethod(c, self.name, self.descriptor)
if method == nil {
panic("java.lang.NoSuchMethodError")
}
self.method = method
}
```
這里面省略了驗證的部分,包括檢查解析后的方法是否為空、檢查當前類是否可以訪問該方法,等等。首先我們看到,第一步是找到方法對應的類:
```
func (self *SymRef) ResolvedClass() *Class {
if self.class == nil {
d := self.cp.class
c := d.loader.LoadClass(self.className)
self.class = c
}
return self.class
}
```
在 MethodRef 結構體中包含對應 class 的引用,如果 class 不為空,則可以直接返回;否則會根據類名,使用當前類的類加載器去嘗試加載這個類。最后將加載好的類引用賦給 MethodRef.class。找到了方法所在的類,下一步就是從類中找到這個方法,也就是方法數據在內存中的地址,對應上面的 lookupMethod 方法。查找時,會遍歷類中的方法列表,這塊在類加載的過程中已經完成,下面是方法數據的結構體:
```
type Method struct {
accessFlags uint16
name string
descriptor string
class *Class
maxStack uint
maxLocals uint
code []byte
argSlotCount uint
}
```
這個其實就和 Class 文件中的 Code 屬性類似,這里面省略了異常和其他的一些信息。類加載過程中,會將各個方法的 Code 屬性按照上面的結構保存在內存中,然后將類中所有方法的地址列表保存在 Class 結構體中。當在 Class 結構體中查找指定方法時,只需要遍歷方法列表,然后比較方法名和描述符即可:
```
for c := class; c != nil; c = c.superClass {
for _, method := range c.methods {
if method.name == name && method.descriptor == descriptor {
return method
}
}
}
```
可以看到,查找方法會從當前方法查找,如果找不到,會繼續從父類中查找。除此以外,還會從實現的接口列表中查找,代碼中省略了這部分,還有一些判斷的條件。
最終,如果成功找到了指定方法,就會將方法數據的地址賦給 MethodRef.method,后面對該方法的調用只需要直接返回 MethodRef.method 即可。
### ??? 5:初始化
?????? 真正開始執行Java程序代碼,該步執行方法根據代碼賦值語句,對**類變量和其他資源**? 進行初始化賦值。
?????? 方法:編譯器自動收集類中所有? 類變量的賦值語句和靜態語句合并而成,收集的順序是在程序代碼出現的順序。所以,靜態語句中只能訪問到定義在靜態語句塊之前的變量,在其之后的變量可以賦值(相當于新建并賦值了)但不可以訪問(因為還沒出現)。
?????? 注:由此步我們就可以得知,我們在分析向上轉型的例子時的程序代碼的運行順序了:父類靜態內容——子類靜態內容——父類構造——子類構造——子類方法 。
??? 在經歷了上面5步“加載”階段后,才真正地可以使用class對象或者使用實例對象。使用過后,不再需要用到該類的class對象或者實例對象時,就會把類卸載掉(發生在方法區的垃圾回收:無用類的卸載)。
## ? ? 四:對象的生命周期
? ? ?對象是由類創建出來的,所以對象的生命周期就是包含在類的生命周期中:
? ? ?類加載(5步)——創建類的實例對象——使用對象——對象回收——類卸載
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊