深入理解Java HelloWorld

importnew發表於2014-02-07

  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啟動時,會使用下面三個類載入器:

  1. Bootstrap類載入器:載入位於/jre/lib目錄下的核心Java類庫。它是JVM核心的一部分,並且使用原生程式碼編寫。
  2. 擴充套件類載入器:載入擴充套件目錄中的程式碼(比如/jar/lib/ext)。
  3. 系統類載入器:載入在CLASSPATH上的程式碼。

  所以,HelloWorld類是由系統載入器載入的。當main方法執行時,它會觸發載入其它依賴的類,進行連結和初始化。前提是它們已經存在。

  最後,main()幀被壓入JVM堆疊,並且程式計數器(PC)也進行了相應的設定。然後,PC指示將println()幀壓入JVM堆疊棧頂。當main()方法執行完畢會被彈出堆疊,至此執行過程就結束了。

 參考文件

  1. 載入
  2. 類載入機制
  3. 類載入器

  原文連結: programcreek 翻譯: ImportNew.com - 黃飛飛

相關文章