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()
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)即緩存機制的體現
其實就是一種雙親委派機制,
4、連接
把類的二進制數據合併到JRE中。
4.1驗證
檢測被加載的是否有正確的內部結構,並和其他類協調一致。
4.2準備
爲類的Field分配內存,並設置默認初始值。
4.3解析
將類的二進制數據中的符號引用替換成直接引用。
5、初始化
JVM負責對靜態Field進行初始化:
方式1:聲明靜態Field時指定初始值。
方式2:使用靜態初始化塊指定初始值。例如:
static{
a = 5;//方式2
}
static a = 9 ;//方式1
所以每次最先初始化的總是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