java虚拟机规范-加载、链接与初始化
生活随笔
收集整理的這篇文章主要介紹了
java虚拟机规范-加载、链接与初始化
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
前言
java虛擬機是java跨平臺的基石,本文的描述以jdk7.0為準,其他版本可能會有一些微調(diào)。java代碼本身并不能為jvm識別,實際上在jvm中的表現(xiàn)形式為Class對象,一個java類從字節(jié)碼到能夠在jvm中正常運行,需要經(jīng)過加載-》鏈接-》初始化三個步驟。
引用
- java虛擬機規(guī)范
- 非法向前引用討論
虛擬機的啟動
- java虛擬機的啟動是通過引導類加載器(Bootstrap Class Loader)創(chuàng)建一個初始類來完成,這個類是由虛擬機的具體實現(xiàn)指定。緊接著,JAVA虛擬機鏈接這個初始類,初始化并調(diào)用它的main方法。之后整個執(zhí)行過程都是由對此方法的調(diào)用開始。
- 啟動過程如圖所示:
加載
類加載器層次結(jié)構(gòu)圖
- 在java中,所有的類都是對其第一次使用時,動態(tài)加載到JVM中。當程序創(chuàng)建第一個對類的靜態(tài)成員(方法、變量)的引用時,就會加載這個類。這個證明了構(gòu)造器也是類的靜態(tài)方法,即使在構(gòu)造器之前并沒有使用static關(guān)鍵字,使用new操作符創(chuàng)建類的新對象也會被當做對類的靜態(tài)成員的引用。
定義
- 注意加載只是類加載中的一個階段,在加載階段虛擬機主要做以下三件事情:
- 通過一個類的全限定名來獲取此類的二進制字節(jié)流,(例如class文件中的部分數(shù)據(jù)、zip包、applet等,執(zhí)行該步驟的模塊稱為類加載器)
- 將該字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
- 在加載階段完成之后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需要的格式存儲在方法區(qū)中,然后在內(nèi)存中實例化一個java.lang.class對象,這個對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
- java類的加載是由類加載器來完成的。類加載器分為兩類:
- 啟動類加載器(bootstrap),JVM原生提供,使用C++實現(xiàn)(注意這里特指hotspot虛擬機,有一些不是)
- 用戶自定義類加載器(user-defined),用戶自定義實現(xiàn),繼承自java.lang.ClassLoader類。
- 類的加載方式分為兩種:顯式加載和隱式加載,這兩種方式都是調(diào)用classloader類中的loadClass方法來完成類的實際加載工作的。直接調(diào)用Classloader中的loadClass方法是另外一種不常用的顯式加載類的技術(shù)。
- 顯式加載:使用Class.forname的方式就是顯式加載
- 隱式加載:使用new創(chuàng)建實例就是隱式加載。
- 類加載器有很多用途,例如java熱替換技術(shù),jvm中相同類的隔離等。
鏈接
- 鏈接類或接口包括驗證、準備、解析。其中解析是可選的部分。
- java虛擬機規(guī)范允許靈活的選擇鏈接發(fā)生的時機,但是必須符合以下規(guī)范:
- 在類或者接口被鏈接之前,它必須被成功的加載過
- 在類或者接口初始化之前,它必須被成功的驗證及準備過
- 程序的直接或者間接行為可能會導致鏈接發(fā)生,鏈接過程中檢查到的錯誤應該在請求鏈接的程序處被拋出。
驗證
- 驗證(verification)階段用于確保類或者接口的二進制表示結(jié)構(gòu)是正確的。驗證過程中可能會導致某些額外的類或者接口被加載進來,但是不應該導致它們也需要驗證或者準備。
準備
- 準備(preparation)階段的任務是為類或者接口的靜態(tài)字段分配空間,并用默認值初始化這些字段,這個階段不會執(zhí)行任何的虛擬機字節(jié)碼指令。(注意在初始化階段會有顯式的初始化器來初始化這些靜態(tài)字段,所以準備階段不做這些事情),注意以下幾點:
- 準備階段進行內(nèi)存分配的僅包括類變量(被static修飾),不包括實例變量。實例變量會在對象實例化時隨著對象一起分配到j(luò)ava堆中。
- 示例:
- 這里的初始值“通常情況下”是數(shù)據(jù)類型的零值,例如public static int val=23;,那么在準備階段過后,val的值將設(shè)置為0,而不是23。而把val值賦值為23的putstatic指令是程序被編譯后,存放于類的構(gòu)造器方法之中,所以把val值賦值為23的動作將在初始化階段才會執(zhí)行,但是如果類的字段屬性表中存在ConstantValue屬性(final修飾符),那么在準備階段就會初始化完成,例如public static final int val=23,那么在準備階段的val值就為23.
- 具體的代碼和字節(jié)碼參加下圖:
解析
- 解析(Resolution)是根據(jù)運行時常量池的符號引用來動態(tài)決定具體值的過程,java虛擬機指令(anewarray,checkcast,getfield,getstatic,instanceof,putstatic,new,invokespecial,invokevirutal)等將符號引用指向運行時常量池。執(zhí)行上述任何一條指令都需要對它的符號引用進行解析。
- 本步驟是符號引用,它與直接引用的區(qū)別在于:
- 符號引用只能告訴你怎么無歧義的定位到目標,該值明確規(guī)定在class文件中
- 直接引用是直接指向目標,(指針、偏移量、句柄)。直接引用和虛擬機實現(xiàn)的內(nèi)存布局相關(guān),同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般都不相同。
初始化
- 初始化是類加載的最后一步,在前面的類加載過程中,基本上動作都是由虛擬機主導和控制(除了用戶自定義類加載器)。到了初始化階段才開始真正的執(zhí)行類定義中的JAVA程序代碼(或者說是字節(jié)碼)。初始化階段可以看做是執(zhí)行類構(gòu)造器<cinit()>方法的過程:
- 該方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊(static)合并而成,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到之前定義的變量,但是可以賦值。比如下面的這段代碼:
- 虛擬機會保證父類的方法一定在自雷之前執(zhí)行,因此第一個執(zhí)行該方法的類一定是java.lang.Object。因此父類的賦值操作一定是在子類之前,即必須把父類中的所有賦值操作執(zhí)行完畢后才會執(zhí)行子類的賦值操作。
- 如果一個接口/類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作,那么編譯器就不會為該類生成方法。
- 多個線程去初始化同一個類,那么只有一個線程去執(zhí)行該類的方法,其他線程都需要阻塞等待,注意同一個類加載器中,一個類型只會被初始化一次。
案例
案例一-關(guān)于非法向前引用
- 仍然以上面的代碼為例,非法向前引用這種編譯檢查,是未了防止靜態(tài)字段在被初始化之前就被獨走默認值,這會導致問題難以診斷。可能有細心的讀者會發(fā)現(xiàn)在方法體中不會出現(xiàn)該問題,比如下面的這段代碼:
- 這段代碼不會拋錯,最后執(zhí)行結(jié)果為0.為什么方法和類可以消除向前引用,而變量不可以呢?這個是因為java運行時為了實現(xiàn)向前引用,在初始化所有字段之前會把所有的字段添加到符號表中,以便可以調(diào)用這些字段。不過由于還沒有初始化這些字段,所以符號表中所有字段都使用默認的值。上圖中的代碼最后返回的額結(jié)果也是0.
案例二
- 上述這段代碼其實是有問題的,真正運行的時候會拋NPE:
- 在java虛擬機啟動的過程前期,加載和鏈接過程都是沒有問題的。但是在初始化的步驟(執(zhí)行方法)中,由于編譯器收集順序是語句在源文件中出現(xiàn)的順序,而res變量的賦值操作在add靜態(tài)語句塊之后。因此在執(zhí)行add方法的時候,res變量只有一個默認值null。因此會導致該段代碼拋NPE
碼字不易,如有建議請掃碼
總結(jié)
以上是生活随笔為你收集整理的java虚拟机规范-加载、链接与初始化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 扬州古城门高挂大红春联迎新春 环卫工受邀
- 下一篇: 01pxc集群的部署