JVM學習第一篇思考:一個Java代碼是怎么運行起來的-上篇
JVM學習第一篇思考:一個Java代碼是怎么運行起來的-上篇
作為一個使用Java語言開發的程序員,我們都知道,要想運行Java程序至少需要安裝JRE(安裝JDK也沒問題)。我們也知道我們Java程序員編寫的程序代碼文件是*.java的,而JRE運行的是*.class的文件。所以,我們需要將java文件編譯成class文件然后才可以。那么,你有沒有想過,一個java文件是怎么運行起來的呢?中間都經歷了哪些環節呢?我們都知道JVM是Java虛擬機,那么,有沒有思考過JVM的內存模型是什么呢?我們new出來的對象,聲明不同類型的變量又是存放在JVM哪個位置呢?
本文是凱哥(凱哥Java:kaigejava)學習JVM系列教程第一篇。歡迎大家一起學習
本文目標:
通過本文學習后,希望大家對JVM類加載過程有個了解。
編輯
上面程序很簡單。那么,有沒有想過上面代碼怎么運行的呢?
選中main方法,然后ruan as...,編譯后,運行輸出。這個流程我想大家都很熟悉的。那么對應的流程應該是什么樣的呢?如下圖:
編輯
在Run的時候,先將.java文件編譯成.class文件。然后,在通過類加載器,將class文件加載到JVM中,然后在運行。輸出結果。
那么為什么編譯好的AppTest.class可以加載到JVM中呢?可以被JVM識別呢?
一個java類的一生都會經歷哪些步驟呢?
如下圖:
編輯
在我們run的時候,AppTest.java類先經過編譯后,編譯成了AppTest.class文件。JVM把class文件加載到內存后需要經歷:加載-驗證-準備-解析-初始化-使用-卸載這七個階段。
第一個問題:JVM在什么時候會加載一個類呢?起始也就是在什么時候會加載.class字節碼文件到JVM的內存中去呢?上面我們寫的,當我們run的時候,才執行的。所以答案就很明確了,就是在你代碼中需要使用到這個類的時候,就去加載的。
具體每一步:
加載
加載階段是將class文件從磁盤或者jar等讀到JVM內存中,并為其創建一個Class對象。任何一個類被使用時候系統都會為其創建一個Class對象的。
加載的同時將加載的這些數據轉換成方法區中運行時數據(運行時候數據區:靜態變量、靜態代碼塊、常量池等),作為方法區數據的訪問入口
這個很好理解的。我要想使用你,需要先得到你,是不是。結合上面我們自己寫的AppTest類。在此階段應該是:
編輯
擴展:
在類加載階段JVM都做了什么?獲取class文件方式都有哪些?
1.1:在類加載的時候JVM完成了以下:
根據類的全路徑(全限定名)來獲取到該類的二進制字節流
(我們知道,在電腦的世界中,什么都是二進制形式存在的)
將加載的字節流中所代表的靜態存儲結構轉換成方法區運行時數據結構
(這個話具體怎么理解,有哪位能留言教教凱哥)
將加載的對象在內存中生成一個代表了該類的jvaa.lang.Class對象。這個Class對象作為加載進來對象在方法區各種數據的訪問入口。
(要想在內存中訪問AppTest這個字節碼類中的屬性或者方法的時候,可以在內存中方法區找到對應的Class對象。這個Class就是入口)
關于方法區在后面文章中,凱哥會詳細講講。
1.2:獲取class文件的方式
可以直接從本地的磁盤文件獲取
可以從忘了下載class文件
可以從ZIP或者jar等文件中
Java源文件動態編譯的class文件
在一個類運行生命周期內,類加載(加載獲取類的二進制字節流)階段,是可控性最強的階段。因為在這個階段,我們程序員可以使用系統提供的類加載去來加載完成,也可以使用自己自定義的類加載來完成.(類加載器在后面文章詳細講講)
1.3:類加載的具體時機,在文章最后,凱哥會列出來。
驗證
將上一步加載到內存中的Class對象進行校驗。確保加載的類的信息符合JVM的規范。確保沒有安全方面的問題。
這個很好理解了,我要使用你,得到你好,我要檢查你是不是符合標準的。如果不合法,就沒法使用。
在此階段如下圖:
編輯
擴展:驗證都驗證哪些方面?
文件給是驗證:驗證加載的字節流是否 符合Class文件格式的規范。
例如:是否已咖啡babe開頭(0xCAFEBABE),主次版八號是否在當前JVM的處理范圍內等等
比如你在JDK1.8下編譯的class文件,放到JDK1.6版本的JVM中,有可能就運行不了的
元數據驗證:對字節碼描述的信息進行語義分析。保證描述信息符合Java語言規范。
例如:這個類如果有父類,是否實現了父類的抽象方法等.
字節碼驗證
符號引用驗證:確保解析動作是正確的。
例如:通過符號引用能找到對應點的類和方法。比如com.kaigejava.Person.getAge()
在比如:符號引用中類、屬性、方法的訪問性是否能被當前類訪問等等。
準備
準備階段,就是給加載進來且驗證通過的Class類分配空間的。這里是給類里面的變量(也就是static修飾的變量)分配空間的,同時給變量一個默認的初始值。
如下圖:
編輯
在準備階段時候static int m 被分配了4個字節的空間,且分配了默認初始值為0(注意默認初始值是0).
PS:int類型占用4個字節。int的默認值是0.如果是對象的話。默認為null
在此階段AppTest.class如下圖:
編輯
該階段需要注意:
在此階段值只對static修飾的靜態變量進行內存分配,賦默認值的(比如0、0L、0D、null、false等);
對于final修飾的靜態字面值常量直接賦初始值(注意:這里的初始值并不是默認值。如果不是字面值靜態常量,那么會和靜態變量一樣賦默認值)
比如:final int x = 1;這個在此階段就給賦值的就是1而不是0
解析
解析是將常量池中的符號引用替換為直接引用(內存地址)的過程。
在此階段AppTest類如下圖:
編輯
擴展:
符號引用:
就是一組符號來描述目標的。可以是任何字面量。這個屬于編譯原理方面的東西。
比如:可以是一個類的完整類名字(com.kaigejava.Person)、字段的名稱和描述符、方法的名稱和描述等。
直接引用:
就是直接指向目標的指針、相對偏移量或者一個間接定位到目標的句柄。比如指向方法區中某一個類的一個指針。
例如:在AppTest這個類中,有個static的靜態變量p。這個靜態變量p又是一個自定義的類型(com.kaigejava.Person),那么在經過解析階段后,這個靜態的p變量將是一個指針(比如0xddff1),這個指針指向該類在方法區的內存地址值。具體見凱哥后續文章,將會詳細講解。
編輯
初始化
到了此階段(初始化階段),JVM才開始真正的執行類中定義的Java代碼。
當進行到初始化階段的時候,就是執行類的構造器
因為父類的
如果一個類中沒有靜態變量或者是靜態的語句塊的時候,編譯器可以不為這個類創建
虛擬機會保證一個類的的
使用
類實例化也初始化成功之后,這個類就是一個正常的類了。我們可以正常使用了。
卸載
當遇到以下幾種情況的時候,類會被卸載
執行了System.exi()方法的時候
程序正常執行結束
程序在執行過程中遇到了異常或者是錯誤而異常終止
由于操作系統出現錯誤導致Java虛擬機進程終止
今天問題:
現在我們知道了一個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中的靜態變量";
String str2 ="父類Father中的非靜態變量";
static {
System.out.println("當前執行了父類Father的靜態代碼塊中的方法");
}
{
System.out.println("執行了父類Father類中的非靜態代碼塊");
}
public Father(){
System.out.println("執行了父類Father中的構造方法了");
}
}
class Son{
static String?str1?= "子類Son中的靜態變量";
String str2 = "子類Son中的非靜態變量";
static{
System.out.println("執行了子類son中的靜態代碼塊");
}
{
System.out.println("執行了子類Son中的非靜態代碼塊");
}
public Son(){
System.out.println("執行了子類son中的構造器方法");
}
}
interface FatherInterface{
static String?str1?= "接口父類FatherInterface中的靜態變量";
void say(String say);
}
class SonInterFace implements FatherInterface{
static String?str1?= "子類SonInterFace中的靜態變量";
String str2 = "子類SonInterFace中的非靜態變量";
static{
System.out.println("執行了子類SonInterFace中的靜態代碼塊");
}
{
System.out.println("執行了子類SonInterFace中的非靜態代碼塊");
}
public SonInterFace(){
System.out.println("執行了子類SonInterFace中的構造器方法");
}
@Override
public void say(String say) {
System.out.println(FatherInterface.str1+"--say:"+say);
}
}
編輯
編輯
運行后答案將在下一篇文章中揭曉。
下一篇預告:
因為這是第一篇,所以只是大致講解了下一個類怎么加載過程。在下一篇文章中,咱們來講解在加載階段使用到類加載器、父類委派機制等、類在什么時候會被初始化等?。歡迎繼續學習。
Java
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。