public class HelloWorld { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("Hello World"); } }
java程序是由類組成的,每一個方法和字段都存在於類中。這是因爲其面向對象的特色所決定的:任何東西都是一個類實例的對象。較之結構化程序語言來講,面向對象的語言有許多優勢,如模塊化、可擴展性等等。html
main方法是一個程序的入口,而且它是靜態的。所謂靜態,指這個方法是屬於類的,而不是某個對象的。java
那又是爲何?爲何咱們不將一個非靜態的方法作爲程序的入口呢?編程
若是一個方法是非靜態的,那麼爲了使用這個方法,咱們必須得去創建一個對象,由於非靜態的方法必須由對象來調用。對於入口來講,這顯然是不現實的。所以,程序的入口方法是靜態的。數組
參數"String[] args"則代表有一個字符串的數組參數將會關聯至程序中,幫助程序進行初始化。oracle
執行這個程序時,java文件(.java)首先將會被編譯爲java字節碼,而且存儲在.class文件中。jvm
那麼這個字節碼長的什麼樣子呢?編程語言
java字節碼自己是不可讀的,可是咱們若是用一個hex編輯器打開的話,它將呈現以下:編輯器
在上圖中的字節碼裏,咱們能看到有許多操做碼(CA,4C等等),它們各自都有本身相關的記憶碼(至關於助記符,以下面將會出現的aload_0).操做碼是不可讀的,可是咱們能夠用javap指令來看看.class文件中的助記符究竟是什麼樣的。模塊化
"javap -c"將會爲每一個類中的方法打印出反彙編代碼。反彙編代碼指的是包含衆多java字節碼的指令集。函數
javap -classpath . -c HelloWorld
Compiled from "HelloWorld.java"public class HelloWorld extends java.lang.Object{public HelloWorld(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; //String Hello World 5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return}
上面的代碼包含了兩個方法:一個是默認構造函數,它是由編譯器自動執行的;另一個就是main方法。
在每個方法下,都有一系列連續的指令集,例如aload_0,invokespecial #1等等。每一個指令表示的函數能夠參照java字節碼指令表。例如,aload_0指的是從局部變量0加載一個引用至棧頂,getstatic則是獲取一個類的靜態字段。須要注意,在getstatic指令後的"#2"指的是運行時的常量池。常量池是JVM運行時的數據存儲區域(JVM Run Time Data Areas)。利用"javap -verbose"命令將會讓咱們看到常量池。
此外,每一個指令的開始都伴隨一個數字,如0,1,4等。在.class文件中,每個方法都有一個相關的字節碼數組。這些數字則對應着操做碼和它的參數存儲在字節碼數組的索引位置。每一個操做碼都是一個字節的長度,而且指令集能夠有0個或者更多的參數。這就是爲何這些數字並非連續的緣由。
如今咱們就用"javap -verbose"來深度瞭解下類吧。
javap -classpath . -verbose HelloWorld
Compiled from "HelloWorld.java"public class HelloWorld extends java.lang.Object SourceFile: "HelloWorld.java" minor version: 0 major version: 50 Constant pool:const #1 = Method #6.#15; // java/lang/Object."<init>":()Vconst #2 = Field #16.#17; // java/lang/System.out:Ljava/io/PrintStream;const #3 = String #18; // Hello Worldconst #4 = Method #19.#20; // java/io/PrintStream.println:(Ljava/lang/String;)Vconst #5 = class #21; // HelloWorldconst #6 = class #22; // java/lang/Objectconst #7 = Asciz <init>;const #8 = Asciz ()V;const #9 = Asciz Code;const #10 = Asciz LineNumberTable;const #11 = Asciz main;const #12 = Asciz ([Ljava/lang/String;)V;const #13 = Asciz SourceFile;const #14 = Asciz HelloWorld.java;const #15 = NameAndType #7:#8;// "<init>":()Vconst #16 = class #23; // java/lang/Systemconst #17 = NameAndType #24:#25;// out:Ljava/io/PrintStream;const #18 = Asciz Hello World;const #19 = class #26; // java/io/PrintStreamconst #20 = NameAndType #27:#28;// println:(Ljava/lang/String;)Vconst #21 = Asciz HelloWorld;const #22 = Asciz java/lang/Object;const #23 = Asciz java/lang/System;const #24 = Asciz out;const #25 = Asciz Ljava/io/PrintStream;;const #26 = Asciz java/io/PrintStream;const #27 = Asciz println;const #28 = Asciz (Ljava/lang/String;)V; {public HelloWorld(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 2: 0 public static void main(java.lang.String[]); Code: Stack=2, Locals=1, Args_size=1 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; //String Hello World 5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 9: 0 line 10: 8}
JVM規範(JVM Specification)中指出:儘管運行時常量池較之一個典型的符號表(symbol table)來講,它包含着一段較大的存儲區域,可是運行時常量池仍然被傳統的編程語言充當用做一個相似符號表的功能。
在"invokespecial #1"中的"#1"指:"#1"常量在常量池中。這個#1號常量就是"Method #6.#15;"(這裏就很明顯看出invokespecial是在調用java.lang.Object的初始化方法)。利用這些數字,咱們就能不斷的取得不可變的常量(final constant)。
而LineNumberTable爲編譯器提供了一些信息去指明哪行java源代碼對應哪一個字節碼指令。例如,在java源代碼中的第9行在main方法中對應的字節碼爲0,而第10行代碼則對應字節碼8。
若是你想了解更多的字節碼,那你應該創建一個複雜點的類來編譯它,HelloWorld畢竟只能做爲說明而已。
如今來換個角度,問題是:JVM是如何加載類和調用類的main方法的呢?
在main方法執行以前,JVM須要加載,連接,初始化一個類。
a.加載是將類或者接口轉換爲二進制而且存入JVM中。
連接則將會把二進制數據包含至運行狀態的JVM中。
b.連接分爲三步:驗證、準備、解析。
驗證要確保類或者接口的內部結構的正確性;
準備則爲類或接口分配所需的內存;
解析是將二進制中的符號替換成直接引用。
c.初始化則將會爲類中的變量分配適當的初始值。
Before the main method is executed, JVM needs to 1) load, 2) link, and 3) initialize the class. 1) Loading brings binary form for a class/interface into JVM. 2) Linking incorporates the binary type data into the run-time state of JVM. Linking consists of 3 steps: verification, preparation, and optional resolution. Verification ensures the class/interface is structurally correct; preparation involves allocating memory needed by the class/interface; resolution resolves symbolic references. And finally 3) initialization assigns the class variables with proper initial values.
加載工做是由java的加載器執行的。當JVM開始時,三種類加載器將會被使用:
1.根加載器(Bootstrap class loader):加載java的核心庫,核心庫位於/jre/lib目錄下,它是JVM的核心的一部分,而且是用的本地代碼。
2.擴展加載器(Extensions class loader:):加載外部jar包,用以擴展核心庫。加載目錄爲/jre/lib/ext
3.系統加載器(System class loader):加載CLASSPATH環境變量中能找到的全部類。
Bootstrap class loader: loads the core Java libraries located in the
/jre/lib directory. It is a part of core JVM, and is written in native code.Extensions class loader: loads the code in the extension directories(e.g.,
/jar/lib/ext).System class loader: loads code found on CLASSPATH.
因此,HelloWorld類是被系統加載器加載的。當main函數被執行時,它將觸發加載,連接,初始化以及其餘與類操做相關的動做。
最後,main()將會被push至JVM棧中,程序計數器PC也將會被設置。緊接着PC指明須要將println()加入JVM棧中。當main方法完成後,它將彈出棧頂元素,函數的執行也就此完畢。