JVM學(xué)習(xí)第一篇思考:一個Java代碼是怎么運行起來的-上篇
JVM學(xué)習(xí)第一篇思考:一個Java代碼是怎么運行起來的-上篇
作為一個使用Java語言開發(fā)的程序員,我們都知道,要想運行Java程序至少需要安裝JRE(安裝JDK也沒問題)。我們也知道我們Java程序員編寫的程序代碼文件是*.java的,而JRE運行的是*.class的文件。所以,我們需要將java文件編譯成class文件然后才可以。那么,你有沒有想過,一個java文件是怎么運行起來的呢?中間都經(jīng)歷了哪些環(huán)節(jié)呢?我們都知道JVM是Java虛擬機,那么,有沒有思考過JVM的內(nèi)存模型是什么呢?我們new出來的對象,聲明不同類型的變量又是存放在JVM哪個位置呢?
本文是凱哥學(xué)習(xí)JVM系列教程第一篇。歡迎大家一起學(xué)習(xí)
本文目標(biāo):
通過本文學(xué)習(xí)后,希望大家對JVM類加載過程有個了解。

編輯
上面程序很簡單。那么,有沒有想過上面代碼怎么運行的呢?
選中main方法,然后ruan as...,編譯后,運行輸出。這個流程我想大家都很熟悉的。那么對應(yīng)的流程應(yīng)該是什么樣的呢?如下圖:

編輯
在Run的時候,先將.java文件編譯成.class文件。然后,在通過類加載器,將class文件加載到JVM中,然后在運行。輸出結(jié)果。
那么為什么編譯好的AppTest.class可以加載到JVM中呢?可以被JVM識別呢?
一個java類的一生都會經(jīng)歷哪些步驟呢?
如下圖:

編輯
在我們run的時候,AppTest.java類先經(jīng)過編譯后,編譯成了AppTest.class文件。JVM把class文件加載到內(nèi)存后需要經(jīng)歷:加載-驗證-準(zhǔn)備-解析-初始化-使用-卸載這七個階段。
第一個問題:JVM在什么時候會加載一個類呢?起始也就是在什么時候會加載.class字節(jié)碼文件到JVM的內(nèi)存中去呢?上面我們寫的,當(dāng)我們run的時候,才執(zhí)行的。所以答案就很明確了,就是在你代碼中需要使用到這個類的時候,就去加載的。
具體每一步:
加載
加載階段是將class文件從磁盤或者jar等讀到JVM內(nèi)存中,并為其創(chuàng)建一個Class對象。任何一個類被使用時候系統(tǒng)都會為其創(chuàng)建一個Class對象的。
加載的同時將加載的這些數(shù)據(jù)轉(zhuǎn)換成方法區(qū)中運行時數(shù)據(jù)(運行時候數(shù)據(jù)區(qū):靜態(tài)變量、靜態(tài)代碼塊、常量池等),作為方法區(qū)數(shù)據(jù)的訪問入口
這個很好理解的。我要想使用你,需要先得到你,是不是。結(jié)合上面我們自己寫的AppTest類。在此階段應(yīng)該是:

編輯
擴展:
在類加載階段JVM都做了什么?獲取class文件方式都有哪些?
1.1:在類加載的時候JVM完成了以下:
根據(jù)類的全路徑(全限定名)來獲取到該類的二進制字節(jié)流
(我們知道,在電腦的世界中,什么都是二進制形式存在的)
將加載的字節(jié)流中所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換成方法區(qū)運行時數(shù)據(jù)結(jié)構(gòu)
(這個話具體怎么理解,有哪位能留言教教凱哥)
將加載的對象在內(nèi)存中生成一個代表了該類的jvaa.lang.Class對象。這個Class對象作為加載進來對象在方法區(qū)各種數(shù)據(jù)的訪問入口。
(要想在內(nèi)存中訪問AppTest這個字節(jié)碼類中的屬性或者方法的時候,可以在內(nèi)存中方法區(qū)找到對應(yīng)的Class對象。這個Class就是入口)
關(guān)于方法區(qū)在后面文章中,凱哥會詳細講講。
1.2:獲取class文件的方式
可以直接從本地的磁盤文件獲取
可以從忘了下載class文件
可以從ZIP或者jar等文件中
Java源文件動態(tài)編譯的class文件
在一個類運行生命周期內(nèi),類加載(加載獲取類的二進制字節(jié)流)階段,是可控性最強的階段。因為在這個階段,我們程序員可以使用系統(tǒng)提供的類加載去來加載完成,也可以使用自己自定義的類加載來完成.(類加載器在后面文章詳細講講)
1.3:類加載的具體時機,在文章最后,凱哥會列出來。
驗證
將上一步加載到內(nèi)存中的Class對象進行校驗。確保加載的類的信息符合JVM的規(guī)范。確保沒有安全方面的問題。
這個很好理解了,我要使用你,得到你好,我要檢查你是不是符合標(biāo)準(zhǔn)的。如果不合法,就沒法使用。
在此階段如下圖:

編輯
擴展:驗證都驗證哪些方面?
文件給是驗證:驗證加載的字節(jié)流是否 符合Class文件格式的規(guī)范。
例如:是否已咖啡babe開頭(0xCAFEBABE),主次版八號是否在當(dāng)前JVM的處理范圍內(nèi)等等
比如你在JDK1.8下編譯的class文件,放到JDK1.6版本的JVM中,有可能就運行不了的
元數(shù)據(jù)驗證:對字節(jié)碼描述的信息進行語義分析。保證描述信息符合Java語言規(guī)范。
例如:這個類如果有父類,是否實現(xiàn)了父類的抽象方法等.
字節(jié)碼驗證
符號引用驗證:確保解析動作是正確的。
例如:通過符號引用能找到對應(yīng)點的類和方法。比如com.kaigejava.Person.getAge()
在比如:符號引用中類、屬性、方法的訪問性是否能被當(dāng)前類訪問等等。
準(zhǔn)備
準(zhǔn)備階段,就是給加載進來且驗證通過的Class類分配空間的。這里是給類里面的變量(也就是static修飾的變量)分配空間的,同時給變量一個默認(rèn)的初始值。
如下圖:

編輯
在準(zhǔn)備階段時候static int m 被分配了4個字節(jié)的空間,且分配了默認(rèn)初始值為0(注意默認(rèn)初始值是0).
PS:int類型占用4個字節(jié)。int的默認(rèn)值是0.如果是對象的話。默認(rèn)為null
在此階段AppTest.class如下圖:

編輯
該階段需要注意:
在此階段值只對static修飾的靜態(tài)變量進行內(nèi)存分配,賦默認(rèn)值的(比如0、0L、0D、null、false等);
對于final修飾的靜態(tài)字面值常量直接賦初始值(注意:這里的初始值并不是默認(rèn)值。如果不是字面值靜態(tài)常量,那么會和靜態(tài)變量一樣賦默認(rèn)值)
比如:final int x = 1;這個在此階段就給賦值的就是1而不是0
解析
解析是將常量池中的符號引用替換為直接引用(內(nèi)存地址)的過程。
在此階段AppTest類如下圖:

編輯
擴展:
符號引用:
就是一組符號來描述目標(biāo)的。可以是任何字面量。這個屬于編譯原理方面的東西。
比如:可以是一個類的完整類名字(com.kaigejava.Person)、字段的名稱和描述符、方法的名稱和描述等。
直接引用:
就是直接指向目標(biāo)的指針、相對偏移量或者一個間接定位到目標(biāo)的句柄。比如指向方法區(qū)中某一個類的一個指針。
例如:在AppTest這個類中,有個static的靜態(tài)變量p。這個靜態(tài)變量p又是一個自定義的類型(com.kaigejava.Person),那么在經(jīng)過解析階段后,這個靜態(tài)的p變量將是一個指針(比如0xddff1),這個指針指向該類在方法區(qū)的內(nèi)存地址值。具體見凱哥后續(xù)文章,將會詳細講解。

編輯
初始化
到了此階段(初始化階段),JVM才開始真正的執(zhí)行類中定義的Java代碼。
當(dāng)進行到初始化階段的時候,就是執(zhí)行類的構(gòu)造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中的所有類變量賦值動作和靜態(tài)語句。
<clinit>()方法與類的構(gòu)造器不同。此方法不需要顯示的調(diào)用類的父構(gòu)造器(如果類有父類的話),虛擬機會保證在子類的<clinit>()方法執(zhí)行前,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。因此JVM中第一個被執(zhí)行的<clinit>()方法的類肯定是java.lang.Object(因為Java中所有類的父類是Object類)
因為父類的<clinit>()方法先執(zhí)行,所以也就意味著父類中定義的static語句塊要優(yōu)先于子類的變量賦值操作
如果一個類中沒有靜態(tài)變量或者是靜態(tài)的語句塊的時候,編譯器可以不為這個類創(chuàng)建<clinit>()方法的
虛擬機會保證一個類的的<clinit>()方法在多線程環(huán)境中被正確的加鎖和同步。多線程訪問,一個訪問,其他在訪問的話會被阻塞。
使用
類實例化也初始化成功之后,這個類就是一個正常的類了。我們可以正常使用了。
卸載
當(dāng)遇到以下幾種情況的時候,類會被卸載
執(zhí)行了System.exi()方法的時候
程序正常執(zhí)行結(jié)束
程序在執(zhí)行過程中遇到了異常或者是錯誤而異常終止
由于操作系統(tǒng)出現(xiàn)錯誤導(dǎo)致Java虛擬機進程終止
今天問題:
現(xiàn)在我們知道了一個Java類是怎么運行起來的了。那么請看下面代碼,運行后輸出的順序是什么?
public class JvmDemo { public static void main(String[] args) { Son son = new Son(); FatherInterface fatherInterface = new SonInterFace(); fatherInterface.say("凱哥Java"); } } class Father{ static String st1 = "父類Father中的靜態(tài)變量"; String str2 ="父類Father中的非靜態(tài)變量"; static { System.out.println("當(dāng)前執(zhí)行了父類Father的靜態(tài)代碼塊中的方法"); } { System.out.println("執(zhí)行了父類Father類中的非靜態(tài)代碼塊"); } public Father(){ System.out.println("執(zhí)行了父類Father中的構(gòu)造方法了"); } } class Son{ static String str1 = "子類Son中的靜態(tài)變量"; String str2 = "子類Son中的非靜態(tài)變量"; static{ System.out.println("執(zhí)行了子類son中的靜態(tài)代碼塊"); } { System.out.println("執(zhí)行了子類Son中的非靜態(tài)代碼塊"); } public Son(){ System.out.println("執(zhí)行了子類son中的構(gòu)造器方法"); } } interface FatherInterface{ static String str1 = "接口父類FatherInterface中的靜態(tài)變量"; void say(String say); } class SonInterFace implements FatherInterface{ static String str1 = "子類SonInterFace中的靜態(tài)變量"; String str2 = "子類SonInterFace中的非靜態(tài)變量"; static{ System.out.println("執(zhí)行了子類SonInterFace中的靜態(tài)代碼塊"); } { System.out.println("執(zhí)行了子類SonInterFace中的非靜態(tài)代碼塊"); } public SonInterFace(){ System.out.println("執(zhí)行了子類SonInterFace中的構(gòu)造器方法"); } @Override public void say(String say) { System.out.println(FatherInterface.str1+"--say:"+say); } } |

編輯

編輯
運行后答案將在下一篇文章中揭曉。
下一篇預(yù)告:
因為這是第一篇,所以只是大致講解了下一個類怎么加載過程。在下一篇文章中,咱們來講解在加載階段使用到類加載器、父類委派機制等、類在什么時候會被初始化等?。歡迎繼續(xù)學(xué)習(xí)。
