這是所有Java程序員知道的程序,它很簡單,但是這樣一個簡單的開始可以帶領我們更深的理解更多複雜的 概念。在這篇文章中,將探索我們可以從這個簡單的程序中學到什麼。
HelloWorld.java
public class HelloWorld {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Hello World");
}
}
1、爲什麼所有東西都從類開始?
Java程序被一個類構成,所有方法和字段都在一個類中。這是由於Java的面向對象的特徵:一切對象都是一個類的實例。面向對象的編程語言對於面向函數式的編程語言而言有許多優點,例如更好的模塊化,可擴展性等等。
2、爲什麼總是有一個“Main”方法?
Main方法是一個程序的入口且是靜態的,靜態意味着這個方法是類的一部分,而不是對象的一部分。
爲什麼這樣做呢,我們爲什麼不用一個非靜態的方法作爲一個程序的入口?
如果一個方法是非靜態的,我們要實用這個方法時必須首先創建一個對象,因爲方法必須在一個對象上被調用。用於程序的入口,顯然是不現實的。如果沒有雞,我們就得不到蛋,因此,程序入口方法應該是靜態的。
參數“String[] args”表明可以傳送一個字符串數組幫助程序的初始化。
3、HelloWorld的字節碼
執行一個程序時,Java文件首先被編譯成Java字節碼文件存儲在.class文件中,字節碼文件時什麼樣的?字節碼文件本身是不可讀的,如果我們使用十六進制編輯器,它們看起來是這樣的:
我們可以在上面的字節碼中看到許多操作碼(例如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中的一個運行時數據池,可以通過“ javap -verbose”產看常量池。
另外,每條指令從一個數字開始,例如0,1,4等。在.class文件中,每個函數有一個相應的字節碼數組,這些數字對應存儲這些操作碼和它們的參數的數組索引。每個操作碼是1 byte的長度,指令可以有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>":()V
const #2 = Field #16.#17; // java/lang/System.out:Ljava/io/PrintStream;
const #3 = String #18; // Hello World
const #4 = Method #19.#20; // java/io/PrintStream.println:(Ljava/lang/String;)V
const #5 = class #21; // HelloWorld
const #6 = class #22; // java/lang/Object
const #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>":()V
const #16 = class #23; // java/lang/System
const #17 = NameAndType #24:#25;// out:Ljava/io/PrintStream;
const #18 = Asciz Hello World;
const #19 = class #26; // java/io/PrintStream
const #20 = NameAndType #27:#28;// println:(Ljava/lang/String;)V
const #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的運行規範中寫道:運行常量池是一個類似傳統編程語言的函數符號表,儘管它包含了一個比典型符號表更廣泛的數據。
在“invokespecial#1”指令中的#1指向常量池中的#1常量。常量是 #6 #15,通過這些數字,我們就可以遞歸的找到最終的常量。
LineNumberTable可以給調試器提供信息以表明Java源代碼對應於哪一行的字節碼指令。例如Java源代碼的第九行對應於字節碼0,第10行對應於字節碼8.
如果你想知道更多關於字節碼的信息,你可以創建和編譯一個更加複雜的類來看看,HelloWorld只是這方面的開始。
4、如何在JVM中執行?
現在的問題是JVM如何加載類並且調用Main方法?
在Main方法執行之前,JVM需要三步
1)加載 。2)鏈接。3)初始化這個類。
1)將一個類或者接口的二進制形式加載到JVM
2)鏈接包含二進制類型的數據到運行態的JVM,鏈接包括三個步驟:驗證,準備和可選的解決方案。驗證是確保類和接口的格式和結構正確;準備包括給類和接口分配所需要的內存;決議解析符號引用。
3)初始化給類變量分配合適的初始值。
這個工作由Java的類加載器(Classloader)完成,當JVM啓動的時候,有三個類加載器被使用:
1、Bootstrap class loader:加位於/jre/lib目錄下的Java核心庫,這是JVM核心的一部分,是用原生代碼編寫的。
2、Extensions class loader:加載擴展目錄中的代碼(例如:/jar/lib/ext)
3、System class loader:加載CLASSPATH中的代碼
所以HelloWorld 類被系統的類加載器所加載。當Main方法執行的時候,如果還有其他相關類存在,類加載器將會加載、鏈接、和初始化它們。
最後。Main() 框架 被壓入JVM堆棧,並相應的設置程序計數器(PC)。程序計數器指示將Println()框架壓入JVM堆棧。當Main()方法完成後,它將被彈出堆棧,執行結束。