深入理解Java HelloWorld
HelloWorld是每個Java程式設計師都知道的程式。它很簡單,但是簡單的開始可以引導你去深入瞭解更復雜的東西。這篇文章將探究從這個HelloWorld這個簡單程式中可以學到的東西。如果你對HelloWorld有獨到的理解,歡迎留下你的評論。
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方法是程式的入口,並且是靜態方法。static關鍵字意味著這個方法是類的一部分,而不是例項物件的一部分。為什麼會這樣呢? 為什麼我們不用一個非靜態的方法作為程式的入口呢?
如果一個方法不是靜態的,那麼物件需要先被建立好以後才能使用這個方法。因為這個方法必須要在一個物件上呼叫。對於一個入口來說,這是不現實的。因此,程式的入口方法是靜態的。
引數 “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個位元組長度,並且指令可以有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常量。這個常量是 “Method #6.#15;”。通過這個數字,我們可以遞迴地得到最終的常量。
LineNumberTable為偵錯程式提供了用來指示Java原始碼與位元組碼指令之間的對應資訊。例如,Java原始碼中的第9行對應於main方法中的位元組碼0,並且第10行對應於位元組碼8。
如果想要了解更多關於位元組碼的內容,可以建立一個更加複雜的類進行編譯和檢視,HelloWorld真的只是一個開始。
4、HelloWorld在JVM中是如何執行的?
現在的問題是JVM是怎樣載入這個類並呼叫main方法?
在main方法執行之前, JVM需要載入、連結以及初始化這個類。
1. 載入將類/介面的二進位制形式裝入JVM中。
2. 連結將二進位制型別的資料融入到JVM的執行時。連結由3個步驟組成:驗證、準備、以及解析(可選)。驗證確保類、介面在結構上是正確的;準備涉及到為類、介面分配所需要的記憶體;解析是解析符號引用。
3. 最後,初始化為類變數分配正確的初始值。
載入工作是由Java類載入器來完成的。當JVM啟動時,會使用下面三個類載入器:
- Bootstrap類載入器:載入位於/jre/lib目錄下的核心Java類庫。它是JVM核心的一部分,並且使用原生程式碼編寫。
- 擴充套件類載入器:載入擴充套件目錄中的程式碼(比如/jar/lib/ext)。
- 系統類載入器:載入在CLASSPATH上的程式碼。
所以,HelloWorld類是由系統載入器載入的。當main方法執行時,它會觸發載入其它依賴的類,進行連結和初始化。前提是它們已經存在。
最後,main()幀被壓入JVM堆疊,並且程式計數器(PC)也進行了相應的設定。然後,PC指示將println()幀壓入JVM堆疊棧頂。當main()方法執行完畢會被彈出堆疊,至此執行過程就結束了。
參考文件
原文連結: programcreek 翻譯: ImportNew.com - 黃飛飛
相關文章
- 深入理解Java PriorityQueueJava
- 深入理解 Java 方法Java
- 深入理解Java反射Java反射
- Java:IO:深入理解Java
- 深入理解 Java 註解Java
- 深入理解 Java 泛型Java泛型
- 深入理解 Java 列舉Java
- Java集合——深入理解HashMapJavaHashMap
- 深入理解 Java 陣列Java陣列
- 深入理解Java異常Java
- 深入理解Java反射(一)Java反射
- 深入理解Java泛型Java泛型
- 邁進Java:HelloWorldJava
- 深入理解 Java 序列化Java
- 深入理解Java I/O模型Java模型
- 深入理解Java中的鎖Java
- 深入理解 Java 中的 LambdaJava
- 深入理解Java中的AQSJavaAQS
- Java基礎——深入理解反射Java反射
- Java 集合深入理解(3):CollectionJava
- Java hashCode() 方法深入理解Java
- 深入理解Java的==和equalsJava
- 深入探討、理解Java的CLASSPATHJava
- 深入理解Java物件結構Java物件
- Java RMI之HelloWorld篇Java
- Java 乾貨之深入理解Java泛型Java泛型
- 深入理解JVM(③)Java的模組化JVMJava
- 深入理解 Java 中 SPI 機制Java
- Java異常機制深入理解Java
- 深入理解Java中的逃逸分析Java
- 深入理解Java中的鎖(一)Java
- 深入理解Java中的鎖(二)Java
- 深入理解Java中的Garbage CollectionJava
- 深入理解java虛擬機器Java虛擬機
- 從Java角度深入理解KotlinJavaKotlin
- Java基礎——深入理解泛型Java泛型
- 深入理解JVM(八)——java堆分析JVMJava
- 深入理解Java物件序列化Java物件