類加載的過程,對象的建立過程 放一塊總結java
Class 文件須要加載到虛擬機中以後才能運行和使用,那麼虛擬機是如何加載這些 Class 文件呢?git
系統加載 Class 類型的文件主要三步:加載->鏈接->初始化。鏈接過程又可分爲三步:驗證->準備->解析。程序員
類加載過程的第一步,主要完成下面3件事情:算法
虛擬機規範多上面這3點並不具體,所以是很是靈活的。好比:"經過全類名獲取定義此類的二進制字節流" 並無指明具體從哪裏獲取、怎樣獲取。好比:比較常見的就是從 ZIP 包中讀取(往後出現的JAR、EAR、WAR格式的基礎)、其餘文件生成(典型應用就是JSP)等等。數組
一個非數組類的加載階段(加載階段獲取類的二進制字節流的動做)是可控性最強的階段,這一步咱們能夠去完成還能夠自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass()
方法)。數組類型不經過類加載器建立,它由 Java 虛擬機直接建立。安全
類加載器、雙親委派模型也是很是重要的知識點,這部份內容會在後面的文章中單獨介紹到。markdown
加載階段和鏈接階段的部份內容是交叉進行的,加載階段還沒有結束,鏈接階段可能就已經開始了。數據結構
對文件格式,元數據信息,字節碼驗證,符號引用進行驗證多線程
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:併發
public static int value=111
,那麼 value 變量在準備階段的初始值就是 0 而不是111(初始化階段纔會賦值)。特殊狀況:好比給 value 變量加上了 fianl 關鍵字public static final int value=111
,那麼準備階段 value 的值就被賦值爲 111。解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。
符號引用就是一組符號來描述目標,能夠是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統須要明確知道這個方法所在的位置。Java 虛擬機爲每一個類都準備了一張方法表來存放類中全部的方法。當須要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就能夠直接調用該方法了。經過解析操做符號引用就能夠直接轉變爲目標方法在類中方法表的位置,從而使得方法能夠被調用。
綜上,解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,也就是獲得類或者字段、方法在內存中的指針或者偏移量。
初始化是類加載的最後一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 <clinit> ()
方法的過程。
對於<clinit>()
方法的調用,虛擬機會本身確保其在多線程環境中的安全性。由於 <clinit>()
方法是帶鎖線程安全,因此在多線程環境下進行類初始化的話可能會引發死鎖,而且這種死鎖很難被發現。
對於初始化階段,虛擬機嚴格規範了有且只有5種狀況下,必須對類進行初始化(只有主動去使用類纔會初始化類):
java.lang.reflect
包的方法對類進行反射調用時如Class.forname("..."),newInstance()等等。 ,若是類沒初始化,須要觸發其初始化。卸載類即該類的Class對象被GC。
卸載類須要知足3個要求:
因此,在JVM生命週期類,由jJVM自帶的類加載器加載的類是不會被卸載的。可是由咱們自定義的類加載器加載的類是可能被卸載的。
只要想通一點就行了,jdk自帶的BootstrapClassLoader,PlatformClassLoader,AppClassLoader負責加載jdk提供的類,因此它們(類加載器的實例)確定不會被回收。而咱們自定義的類加載器的實例是能夠被回收的,因此使用咱們自定義加載器加載的類是能夠被卸載掉的。
虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,而且檢查這個符號引用表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。分配方式有 「指針碰撞」 和 「空閒列表」 兩種,選擇哪一種分配方式由 Java 堆是否規整決定,而 Java 堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種方式:(補充內容,須要掌握)
選擇以上兩種方式中的哪種,取決於 Java 堆內存是否規整。而 Java 堆內存是否規整,取決於 GC 收集器的算法是"標記-清除",仍是"標記-整理"(也稱做"標記-壓縮"),值得注意的是,複製算法內存也是規整的
內存分配併發問題(補充內容,須要掌握)
在建立對象的時候有一個很重要的問題,就是線程安全,由於在實際開發過程當中,建立對象是很頻繁的事情,做爲虛擬機來講,必需要保證線程是安全的,一般來說,虛擬機採用兩種方式來保證線程安全:
內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
初始化零值完成以後,虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
在上面工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛開始,<init>
方法尚未執行,全部的字段都還爲零。因此通常來講,執行 new 指令以後會接着執行 <init>
方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。
在 Hotspot 虛擬機中,對象在內存中的佈局能夠分爲 3 塊區域:對象頭、實例數據和對齊填充。
Hotspot 虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據(哈希碼、GC 分代年齡、鎖狀態標誌等等),另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是那個類的實例。
實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各類類型的字段內容。
對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位做用。 由於 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍,換句話說就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數(1 倍或 2 倍),所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。
創建對象就是爲了使用對象,咱們的 Java 程序經過棧上的 reference 數據來操做堆上的具體對象。對象的訪問方式由虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:
句柄: 若是使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來做爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息;
直接指針: 若是使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。
優劣比較:
**這兩種對象訪問方式各有優點。**使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 自己不須要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。