简易Java(01):从HelloWorld中可以学到什么?
HelloWorld程序是每一个Java程序员都知道的程序。它很简单,但是小事物却包含着大道理,它可以帮助我们更深入的去理解Java中一些更复杂的原理。在这篇文章中,我将向大家说明我们能从这个简单程序中学到什么知识。如果你能从HelloWorld中体会到更多东西,请留言告知。
public class HelloWorld {
/**
* Coder: D瓜哥,http://www.diguage.com/
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Hello World");
}
}
1、为什么一切事物皆从一个类(Class)开始?
Java程序都是从类开始构建的,每一个方法和属性都必须归属在一个类中。这些规定缘于面向对象的特性:每个事物都是一个对象,而对象是类的实例。面向对象的编程语言相比面向过程的编程语言,拥有更多的特性,例如:更好的模块化,更容易扩展等等。
2、为什么这里总有一个main方法?
main方法是这个程序的入口,而且它是一个静态方法。静态方法意味着这个方法是所在类的一部分而不是,该类对象的一部分。
为啥这样呢?为什么我们不将一个非静态方法作为程序的入口呢?
如果一个方法不是静态方法,那么必须先创建一个对象,然后才可以调用这个方法。因为非静态方法必须由一个对象来调用。所以,作为程序的入口,这是不太现实的。因此,程序入口方法是静态的。
参数String[] args表明,可以将一个字符串数组传递给程序来帮助程序进行初始化。
3、HelloWorld的字节码
为了执行该程序,必须先将Java文件编译成字节码,字节码存放到.class为扩展名的文件。
字节码长什么样子呢?
字节码本身是不易读。如果我们使用十六进制编辑器打开,则它看起来如下图所示:

在上面的字节码中,我们可以看到很多操作码(opcode)(例如:CA、4C等),这些操作码都有一个相对应的助记符(例如:将要在下面例子中出现的aload_0)。这些操作码同样不易读,但是我们可以使用javap命令来查看一个.class文件的助记符形式。
javap -c 可以将类中每个方法的反编译成汇编语言形式,然后打印出来。反编译代码即代表着Java字节码的组成结构。
执行如下命令:
javap -classpath . -c HelloWorld
则在终端的输入如下:
Compiled from "HelloWorld.java"
public class HelloWorld {
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 bytecode instruction listings中查找。(译者注:大家也可以看《Java虚拟机规范(Java SE 7中文版)》,电子版下载,纸版书也已经出版)例如,aload_0从局部变量表加载一个 reference 类型值到操作数栈中,getstatic则是获取类的静态字段值。注意紧跟在getstatic指令后面的#2指向运行时常量池。常量池是Java虚拟机(JVM)运行时数据区的一部分。这让我们看一看常量池,可以使用javap -verbose命令来帮助我们查看。
另外,每一个命令都是一个数字开头,例如0、1、4等。在.class文件中,每一个方法都有一个对应的字节码数组。这些数字代表每一个操作码以及参数在数组中的下标。每个操作码有一个字节长,并且指令可以有0个或者多个参数。这就是这些数字不连续的原因。
现在,让我们使用javap -verbose来进一步研究一下这个类。
javap -classpath . -verbose HelloWorld
终端输出如下:
Classfile /Path/to/HelloWorld.class
Last modified 2014-5-11; size 425 bytes
MD5 checksum 5a8c1eaa545b07c8b13d206f4328e01b
Compiled from "HelloWorld.java"
public class HelloWorld
SourceFile: "HelloWorld.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public HelloWorld();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
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 7: 0
line 8: 8
}
该输出与原文有一定的差别。译者猜测可能跟JDK的版本有关。我的JDK版本如下:
java version "1.7.0_55" Java(TM) SE Runtime Environment (build 1.7.0_55-b13) Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)
根据Java虚拟机规范说明:运行时常量池提供了类似传统编程语言的符号表的功能,当然它比一个典型的符号表包含了更多的数据。
在invokespecial #1指令中的#1表示指向常量池中的第一个常量。这个常量是Methodref #6.#15。从这个数字,我们可以递归地得到最终实际的常量。
行号表(LineNumberTable)用于为调试提供信息,用于标示出错指令对应的Java源代码。例如,在Java源代码的第7行对应字节码中的main方法的第0个指令;源代码的第8行对应字节码第8个指令。
如果你想更深入地了解字节码,你可以创建、编译一个更复杂的类来看一看。HelloWorld只是你了解这些的一个小小的开端。
4、Class文件是如何在Java虚拟机中执行的?
最后一个问题,Java虚拟机是如何加载这个类并执行main方法的?
在main方法执行之前,Java虚拟机必须先1)加载(load),2)link(链接),3)初始化(initialize)这个类。1)加载就是将一个二进制形式的类或者接口载入到Java虚拟机中。2)链接将二进制类型的数据转化成Java虚拟机的运行时状态。链接包含三步:校验(verification)、准备(preparation)和解析(Resolution)。校验确保这个类或者接口是结构正确的;准备涉及类或者接口所需内存的分配;解析是将符号应用替换为直接引用。最后,初始化是给变量赋予合适的初始化值。

加载的工作由JavaClassloader来完成。当启动Java虚拟机时,有三个类加载器会被用到:
- 启动类加载器(Bootstrap ClassLoader):加载Java的核心库,这些库存放在
/jre/lib目录下。这是Java虚拟机核心的一部分,这部分使用本地代码编写。 - 扩展类加载器(Extensions ClassLoader):加载在扩展路径中的库。(例如:
/jar/lib/ext) - 系统类加载器(System ClassLoader):加载在
CLASSPATH中发现的代码。
所以,HelloWorld类由系统类加载器来加载。当main方法被执行时,它将触发相关依赖类的加载、链接和初始化,前提是这些依赖的类存在。
最后,main()栈桢加载到到Java虚拟机栈,然后程序计数器(Program Counter,简称PC)开始计数。程序计数器然后指示将println()栈桢加入Java虚拟机栈。当main()方法完成后,它将从Java虚拟机栈弹出,然后执行完成。
D瓜哥在网上看到了一个《Simple Java》(窃译为《简易Java》)文档,主要讲解Java面试题的。感觉不错,所以就翻译过来分享一下。这是第一篇。
参考资料
原文链接:HelloWorld中可以学到什么?">https://wordpress.diguage.com/archives/74.html
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。

Java百问,不是原创吆,学长
不是原创,但是确实是我自己翻译的。
顶下地瓜哥~
确实不是原创。不过,是我翻译的。(我后来上网查了查,第一篇还有人翻译,后面的似乎没人翻译,所以,我觉得搞起! )