Java執行前都幹了什麼?

Java第一個HelloWorld程序,控制檯顯示出HelloWorld之前都做了什麼?越詳細越好。

public class Test{
    public static void main(String[] args){
        System.out.println("HelloWorld!");
    }
}

前言

打開Windows電腦(已經安裝並配置好JDK),新建一個記事本,輸入如上代碼,保存爲Test.java(剛學習java的時候不都是喜歡用Test命名嗎?Test01、Test02的)。打開cmd,編譯執行後,cmd中輸出“HelloWorld!”倒吸一口涼氣,這TM啥玩意?

作爲一個Java工程師,在接觸到Java後總要回過頭來探究這第一個程序。輸出前都做了什麼?(其實我都不想告訴你們這個是我遇到的面試題,崩潰了)。

以下就是簡單的分析過程。(如有不詳細的地方請查看文章最後的後序部分的參考引用文章,如果有錯誤的地方,請提出。本人郵箱:[email protected]

1、windows爲什麼可以運行java

1.1path說明

我們都知道在CMD中運行某些可執行文件(exe)或者批處理文件(bat),一般都是需要先“cd 路徑”進入該文件所在的目錄,或者是使用“絕對路徑+\可執行文件或者批處理文件名”的方式進行運行。如果不想這樣做呢?那就需要配置path環境變量。

Windows在執行用戶命令時,用戶若沒給出絕對路徑,則首先在當前目錄下查找是否存在目標文件;如果找不到,則依次根據path路徑中的路徑尋找目標文件。系統按照第一次找到目標爲準,如果都爲找到,則會出現“ ‘XX’不是內部命令或者外部命令,也不是可執行的程序”。

1.2配置JDK環境變量

配置JDK環境變量JAVA_HOME、CLASSPATH時,JAVA_HOME指向JDK安裝目錄,CLASSPATH指向lib、rt、tools進行加載指定的庫類。path中指向運行的二進制文件夾bin中,使裏面的類似java、javac、javap等都是可以在windows中直接使用。這也是使用JDK運行java之前爲什麼要進行JDK環境變量配置的原因。

2、編譯

使用javac進行編譯(javac源碼請點擊),生成.class二進制文件。以下是javac編譯過程源碼,漢字部分是人爲添加註釋。

/**as a JavaCompiler can only be used once ,throw an exception if it has been used before.
*/
//判斷是否已經編譯過
if(hasBeenUsed)
    throw new AssertionError("attempt to reuse JavaCompiler");
hasBeenUsed = true ;

start_msec = now();
try{
    //準備過程,初始化插入式註解處理器
    initProcessAnnotations(processors);
    //These method calls must be chained to avoid memory leaks
    delegateCompiler = processAnnotations(					//過程2:執行註解處理
        enterTrees(stopIfError(CompileState.PARSE,			//過程1.2:輸入到符號表
        	parseFiles(sourceFileObjects))),classnames);	//過程1.1:分析詞法、語法
    //過程3:分析語義以及生成字節碼
    delegateCompiler.compile2();
    //完成編譯
    delegateCompiler.close();
}

2.1分析和輸入到符號表

Parse and Enter。

2.1.1詞法分析

com.sun.tools.javac.parser.Scanner類實現。源代碼的字符流轉變成標記(token)集合。單個字符是編寫程序的最小單位,標記是編譯過程的最小單位。關鍵字、運算符、字面量、變量名都可看成標記。

2.1.2語法分析

com.sun.tools.javac.parser.Parser類實現 。是根據Token序列構造抽象語法樹的過程,抽象語法樹(Abstract Syntax Tree,AST)是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節點都代表着程序代碼中的語法結構(Construct),例如包、類型、修飾符、運算符、接口、返回值甚至代碼註釋都可以是一個語法結構。抽象語法樹由com.sun.tools.javac.tree.JCTree類表示

2.1.3填充符號表

詳情請參考上一個鏈接。符號表就暫時看成map的k-v即可,在之後生成的class文件中都是這種k-v結構。com.sun.tools.javac.comp.Enter類實現 。.java文件類中如果沒有構造器,這時候會默認生成無參構造器,訪問權限和類一致(和“生成class文件”時生成構造器不是同一回事)。

2.2註解處理

Annotation Processing。參照上圖源碼,插入式註解處理器的初始化過程是在initProcessAnnotations()方法中完成的,而它的執行過程則是在processAnnotations()方法中完成的。

2.3語義分析和生成class文件

Analyse and Generate。進行源代碼是符號邏輯的審查。

2.3.1標記檢查

在這裏語義分析,類似如下會驚醒語法摺疊,把sum的值在語法樹中標記成11;

int sum = 10 + 1 ;

其他的還有類似變量類型賦值是否正確,是否應該強轉。實現類是com.sun.tools.javac.comp.Attr類和com.sun.tools.javac.comp.Check類 。

2.3.2數據及控制流分析

檢測局部變量使用前是否有賦值、方法是否有正確返回值、受查異常是否進行正確處理。

這裏有個問題。就是昨天說的標記問題flag????2018年12月5日01:22:03

2.3.3解語法糖

泛型、變長參數、自動裝箱/拆箱等,虛擬機運行時不支持這些語法,它們在編譯階段還原回簡單的基礎語法結構,這個過程稱爲解語法糖。

desugar()觸發,com.sun.tools.javac.comp.TransTypes類和com.sun.tools.javac.comp.Lower類實現。

2.3.4生成的class文件

com.sun.tools.javac.jvm.Gen類實現。將語法樹和符號表轉成字節碼

實例構造器()方法和類構造器()方法就是在這個階段添加到語法樹之中的(注意,這裏的實例構造器並不是指默認構造函數,如果用戶代碼中沒有提供任何構造函數,那編譯器將會添加一個沒有參數的、訪問性(public、protected、private)與當前類一致的默認構造函數,這個工作在填充符號表階段就已經完成),這兩個構造器的產生過程實際上是一個代碼收斂的過程,編譯器會把語句塊(對於實例構造器而言是“{}”塊,對於類構造器而言是“static{}”塊)、變量初始化(實例變量和類變量)、調用父類的實例構造器(僅僅是實例構造器,()方法中無須調用父類的()方法,虛擬機會自動保證父類構造器的執行,但在()方法中經常會生成調用java.lang.Object的()方法的代碼)等操作收斂到()和方法之中,並且保證一定是按先執行父類的實例構造器,然後初始化變量,最後執行語句塊的順序進行,上面所述的動作由Gen.normalizeDefs()方法來實現。

其次,類似如下代碼會進行轉換

String filePathAndName = "";
String filePath = "D:/java";
String fileName = "test.java";
filePathAndName = filePath+File.separator+fileName ;
/**源碼中String是final的並且是不可以修改的,這裏的上一行代碼在生成字節碼的時候回默認變成以下StringBuffer.append()進行操作*/

反編譯如下圖,可以看出String更改爲StringBuffer或者StringBuilder的append()


1543979783713


com.tools.javac.jvm.ClassWriter的writeClass()方法輸出字節碼

文件具體內容是以“cafebabe”魔數開頭的二進制文件(4個字節),虛擬機根據魔數判斷是否爲.class文件。5-6次版本號,7-8主版本號。版本號對應關係如下:

JDK版本號 Class版本號 16進制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

剩下的內容是類似k-v。詳細情況請參考最下方參考文獻

3、加載

就是把class文件讀入內存,創建一個java.lang.Class的實例對象,因爲所有的類其實都是一種Class的對象。

3.1加載器種類

Bootstrap ClassLoader 根加載器:JVM自身實現,負責加載java核心類。例如:String、System。核心庫都在%JAVA_HOME%/jre/lib/rt.jar中(在eclipse中看不到源碼添加rt.jar即可)。

Extension ClassLoader擴展加載器:主要爲了java擴展新功能,擴展包在%JAVA_HOME%/jre/lib/ext中

System ClassLoader (APP ClassLoader)系統類加載器:加載來自java命令的-classpath選項、java.class.path系統屬性,或CLASSPATH環境變量指定的jar包和類路徑。(一般情況下自定義的類加載器都是以此爲父類加載器)

Custom ClassLoader 用戶自定義加載器:繼承ClassLoader,覆寫findClass()。

3.2加載機制

全盤負責:加載器加載某Class則需要加載其依賴的其他Class,除非限時使用另一個加載器載入。

父類委託:讓父類加載器加載Class,若無法加載則自己加載。(個人理解類似初始化總要初始化父類)

緩存:所有加載過的Class都會被放在緩存區,進行加載Class時先到緩存區尋找是否存在該Class,如果不存在則讀取賭贏的二進制數據,轉成Class放在緩存區。正因爲緩存機制,每次修改Class後需要重啓JVM。例如:更新服務器上的項目,不管使用增量包還是全量包都要重啓項目。最好在更新前停止項目,更新後重啓。(一些特定的xml,html等之類的不需要重啓,也是需要看情況的)。

另外,ClassLoader類中的findLoadedClass(String name)即緩存機制的體現

img

其實就是一種雙親委派機制

4、連接

把類的二進制數據合併到JRE中。

4.1驗證

檢測被加載的是否有正確的內部結構,並和其他類協調一致。

4.2準備

爲類的Field分配內存,並設置默認初始值。

4.3解析

將類的二進制數據中的符號引用替換成直接引用。

5、初始化

JVM負責對靜態Field進行初始化:

方式1:聲明靜態Field時指定初始值。

方式2:使用靜態初始化塊指定初始值。例如:

static{
    a = 5;//方式2
}
static a = 9 ;//方式1

JVM初始化過程圖

所以每次最先初始化的總是java.lang.Object類。

初始化的時機:

​ 1、創建類實例:使用new;使用反射;使用反序列化。

Map<String, String> hashMap =new HashMap();//使用new
Class clazz = Class.forName(“com.mysql.jdbc.Driver”);//使用反射

public Class Person implements Serializable{ //實現Serializable序列化接口
    private static final long serialVersionUID = 1L;//定義serialVersionUID序列化ID
    //省略Person的各種屬性和setter、getter方法
}
//最後使用I/O流ObjectInputStream的readObject()方法,使文件流反序列化成對象

​ 2、調用類的靜態方法。

​ 3、訪問類或者接口的靜態Field或者爲靜態Field賦值。

​ 4、初始化某個類的子類(原因參考上面流程圖)。

​ 5、調用java.exe。

注意:“宏變量”的調用不會初始化其所在類(static final的Field在編譯期間就確定下來),例如:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
後序:

引用部分來自以下文章

原文:《Javac編譯器詳解》 https://blog.csdn.net/tanga842428/article/details/52384127

原文: 《Java Class 文件詳解》 https://www.cnblogs.com/yaoyinglong/p/4297793.html

原文:《瘋狂java講義》 http://www.bubuko.com/infodetail-1803036.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章