學習一門編程語言的三個層次
我覺得對一門語言的理解不能僅僅停留在表面,如果把學習一門編程語言分爲三個層次的話。我認爲大概分爲下面三個方面:
- 第一層 是認識瞭解這門編程語言的語法、用法
- 第二層 是瞭解這門編程語言同其他語言的優劣勢和生態
- 第三層 是瞭解這門編程語言底層運行的機制,能從這樣有利於解決罕見問題和程序調優
Java最核心的還是JVM就是上面說的第三層,其中最重要的就是Java類加載機制。下面我們先看一下Java運行的過程
Java運行過程
首先,我們要了解一件事情,那就是我們編寫的Java代碼是如何運行起來的呢?
這張圖的流程可能很多人都知道。
Java文件通過javac編譯成.class字節碼文件,然後由JVM加載字節碼文件,開始編譯執行,這個加載的過程就是類加載。
JVM主要分爲五塊區域:
-
堆
-
虛擬機棧
-
本地方法棧
-
pc寄存器
-
方法區
下面放一張更詳細的流程圖
Java類的生命週期
這張圖應該很多人已經看到過,但是我今天還是要拿出來講一下,因爲我覺得這張圖本身是不嚴謹的,稍後會說明。
關於這張圖,我覺得有3個方面需要說明一下:
- 這張圖表現了一個類完整的生命週期,而我之前提到的類加載,只包括加載、連接和初始化這三個過程。
- 我們要區分類加載和加載,圖上的加載只是類加載的第一步。
- 這張圖上,連接裏解析環節的順序是靈活的,驗證是分爲很多個步驟,而其他環境的順序固定不可變。比如後面會講到的“後期綁定”,就是發生在初始化環節之後。
接下來先來看加載這個環節。
加載
加載是在硬盤上查找或通過IO讀取.class字節碼文件,並將其轉化爲靜態數據結構存儲在方法區,同時在堆中實例化Class對象,便於用戶調用的java.lang.Class類型的對象的過程。
像一些動態代理技術,就是用的即時計算出來的class,然後實例化代理對象。而上面提到的方法區和堆的概念,可以參照前一篇掌握JVM這一篇文章就夠了,去查看JVM內存模型的說明。
接下來是連接裏的驗證階段。
連接-驗證
首先我要先說明一點,之所以我前面說那張Java類生命週期的圖不嚴謹,其一就是因爲驗證這個動作有很多步驟,其實是分佈在各個不同環節的,並非像圖所示的固定在某個環節。
- 其中第一個步驟,對文件格式的驗證,其實是發生在上面加載環節的,只有驗證通過才能順利加載,這時候JVM方法區就有了Class對象的靜態數據結構,堆中也有了實例化的對象,但這時候需要使用這個類還需要連接。
- 連接的第一步就是對這個類的元數據和字節碼進一步進行驗證,看是否會對JVM造成危害,等驗證通過,JVM暫且認爲這個類是安全的。但這時候驗證並沒有完全結束,還有一步對符號引用的驗證。
- 對符號引用的驗證,是發生在解析環節內的,而解析環節是可以在初始化環節之前或之後進行的,所以驗證是包含了很多步驟,分佈在各個不同環節的。
在元數據和字節碼驗證通過之後,虛擬機就進入到了準備階段。
連接-準備
準備就是爲類靜態變量分配內存,並將其初始化爲默認值。
例:static int b= 10,這裏只將b初始化爲0,10的值會在初始化階段賦值。
但類變量如果被final修飾則會直接賦予定義的值(即final static修飾的變量 final static int c = 15;)
此時會出現一個被太多人混淆和誤解的概念:
我們來好好捋一捋。
“常常看到有人說,JDK8及以後採用“元空間”來替代“方法區”,這種說話時完全錯誤的,方法區是抽象概念,元空間是具體實現方式”
方法區是JVM規範中的定義,只是個概念,無論是元空間還是永久代,都只是方法區的一種實現。
-
jdk7是永久代,用於存放類信息,字符串常量池,靜態變量。
-
jdk8以及以後元空間存放類信息,字符串常量池和靜態變量被放到了堆中。
jrocket和hotspot虛擬機都有方法區的概念,只是jrocket沒有永久代這種實現方式。在jdk8中整合兩大虛擬機之後,由永久代換成了元空間。
在準備完成之後,就來到了之前一直提到的解析環節。
連接-解析
解析就是將符號引用轉化爲直接引用。
如果我們調用的是具體的實現類,那就是靜態解析。
像我們所瞭解的多態,就是通過“後期綁定”來實現的,其實就是這裏的動態解析。比如接口或抽象類,剛開始是不知道具體實現類的,所以在運行過程中,當虛擬機調用棧得到類型信息,這時候就可以用明確的直接引用替換符號引用了,這就是動態解析。這時候解析環節就是發生在初始化之後的。
底層對應的invokedynamic這條字節碼指令。
當解析完成,整個連接步驟就完成了,這時候類就引到了程序中。
接下來是初始化階段。
初始化
初始化階段比較好理解,簡單概括就是:爲類變量初始化賦值,爲成員變量賦零值,執行靜態代碼塊。
下面進入源碼階段,講一下類加載底層的一個詳細流程。
類加載底層詳細流程
- 先執行java文件
- 創建Java虛擬機
- 創建引導類加載器
- 由C++調用Java代碼創建JVM啓動器實例Launcher
- Launcher類由引導類加載器加載
- 同時創建其他類加載器(都是Launcher下的靜態內部類)
- 通過ClassLoader.loadClass()加載類
雙親委派機制
、
默認從Application ClassLoader開始加載類。
在每個ClassLoader開始的時候,先從jvm緩存查找該類。如果緩存沒找到,然後向上委託給父類加載器進行加載,一直到Bootstrap ClassLoader。
父加載器加載失敗則向下由子加載器繼續加載。
-
Bootstrap ClassLoader:負責加載jre/lib目錄下的核心jar包,比如rt.jar等
-
Extension ClassLoader:負責加載jre/lib/ext目錄下的jar包
-
Application ClassLoader:負責加載項目中自己寫的類,就是加載ClassPath/類路徑下的.class字節碼文件
-
自定義加載器:負責加載自定義路徑下的.class字節碼文件