从 “Hello World” 中可以学到什么?

原文地址:http://www.programcreek.com/2013/04/what-can-you-learn-from-a-java-helloworld-program/

这是每个 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 程序都是由类构建的,每个方法和字段都必须在一个类中。 这是由于其面向对象的特性:一切都是类的实例对象。 面向对象编程语言比函数式编程语言有很多优点,如更好的模块化,可扩展性等。

2. 为什么总是有个 "main" 方法?

main 方法是程序入口,它是静态的。 static 表示该方法是其类的一部分,而不是对象的一部分。

这是为什么? 为什么不把非静态方法作为程序入口?

如果方法不是静态的,那么需要先创建一个对象来使用该方法。 因为必须在对象上调用该方法。 为了实现程序入口,这是不现实的。 我们不能得到没有鸡的鸡蛋。 因此,程序入口方法是静态的。

参数 String [] args 表示可以将一个字符串数组发送到程序来帮助程序初始化。

3. HelloWorld 的字节码

要执行程序,Java 文件首先被编译为存储在 .class 文件中的 Java 字节码。 字节代码是什么样的? 字节码本身不可读。 如果我们使用十六进制编辑器打开它,将会显示如下:

我们可以在上面的字节码中看到很多操作码(例如 CA4C 等),每个操作码都具有相应的记忆码(例如下面的例子中的 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
}

上面的代码包含两个方法:一个是默认的构造函数,由编译器推断;另一个是主要的方法。

在每个方法下,都有一系列指令,例如 aload_0invocationpecial#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 文档:运行时常量池为常规编程语言提供类似于符号表的函数,尽管它包含比典型符号表更宽的数据范围。

invocationspecial#1 指令中的 #1 指向常量池中的 #1 常数。常数是 Method #6.#15;。从数字,我们可以通过递归得到最终的常数。

LineNumberTable 向调试器提供信息,以指示 Java 源代码行号对应的那个字节代码指令。 例如,Java 源代码中的第 9 行对应于主方法中的字节代码 0,第 10 行对应于字节代码 8。

如果你想更多地了解字节码,你可以创建和编译一个更复杂的类来看看。 HelloWorld 只是一个初步的认识。

4. 它是如何在 JVM 中执行的?

现在的问题是 JVM 如何加载类并调用 main 方法?

在执行 main 方法之前,JVM 需要 -> 加载 -> 链接 -> 初始化类。 加载将类/接口的二进制形式带入 JVM。 链接将二进制类型数据并入 JVM 的运行时状态。 链接包括三个步骤:验证,准备和可选解决方案。 验证确保类/接口在结构上是正确的; 准备涉及分配类/接口所需的内存; 决议解决了符号引用。 最后初始化使用适当的初始值分配类变量。

这个加载工作由 Java 类加载器(Java Classloaders)完成。 当 JVM 启动时,使用三个类加载器:

  • Bootstrap class loader:加载位于 /jre/lib 目录中的核心 Java 库。 它是核心 JVM 的一部分,并以本地代码编写。
  • Extensions class loader:在扩展目录中加载代码(例如 /jar/lib/ext)。
  • System class loader:加载 CLASSPATH 上的代码。

所以 HelloWorld 类是由系统类加载器加载的。 当 main 方法被执行时,它会触发其他依赖类的加载,链接和初始化(如果它们存在)。

最后,将 main() 框架推入 JVM 堆栈,并相应地设置程序计数器(PC)。 PC 然后指示将 println() 帧推送到 JVM 堆栈。 当 main() 方法完成时,它将从堆栈弹出并执行完成。

参考阅读:

  1. Load
  2. Class Loading Mechanism
  3. Classloader

results matching ""

    No results matching ""