这段程序我想每个java程序员都知道吧。没错,它很简单,但是就这么一个简单的开始却能引发许多深层次的东西。在本文中,我将向大家展示我们能从中学习到什么。如果阅读完后,这段Hello World让你觉得意味繁多,那么请留下您的评论。
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方法是一个程序的入口,并且它是静态的。所谓静态,指这个方法是属于类的,而不是某个对象的。
那又是为什么?为什么我们不将一个非静态的方法做为程序的入口呢?
如果一个方法是非静态的,那么为了使用这个方法,我们必须得去建立一个对象,因为非静态的方法必须由对象来调用。对于入口来说,这显然是不现实的。因此,程序的入口方法是静态的。
参数"String[] args"则表明有一个字符串的数组参数将会关联至程序中,帮助程序进行初始化。
3.HelloWorld的字节码
执行这个程序时,java文件(.java)首先将会被编译为java字节码,并且存储在.class文件中。
那么这个字节码长的什么样子呢?
java字节码本身是不可读的,但是我们如果用一个hex编辑器打开的话,它将呈现如下:
在上图中的字节码里,我们能看到有许多操作码(CA,4C等等),它们各自都有自己相关的记忆码(相当于助记符,如下面将会出现的aload_0).操作码是不可读的,但是我们可以用javap指令来看看.class文件中的助记符到底是什么样的。
"javap -c"将会为每个类中的方法打印出反汇编代码。反汇编代码指的是包含众多java字节码的指令集。
上面的代码包含了两个方法:一个是默认构造函数,它是由编译器自动执行的;另外一个就是main方法。
在每一个方法下,都有一系列连续的指令集,例如aload_0,invokespecial #1等等。每个指令表示的函数可以参照java字节码指令表。例如,aload_0指的是从局部变量0加载一个引用至栈顶,getstatic则是获取一个类的静态字段。需要注意,在getstatic指令后的"#2"指的是运行时的常量池。常量池是JVM运行时的数据存储区域(JVM Run Time Data Areas)。利用"javap -verbose"命令将会让我们看到常量池。
此外,每个指令的开始都伴随一个数字,如0,1,4等。在.class文件中,每一个方法都有一个相关的字节码数组。这些数字则对应着操作码和它的参数存储在字节码数组的索引位置。每个操作码都是一个字节的长度,并且指令集可以有0个或者更多的参数。这就是为什么这些数字并不是连续的原因。
现在我们就用"javap -verbose"来深度了解下类吧。
JVM规范(JVM Specification)中指出:尽管运行时常量池较之一个典型的符号表(symbol table)来说,它包含着一段较大的存储区域,但是运行时常量池仍然被传统的编程语言充当用作一个类似符号表的功能。
在"invokespecial #1"中的"#1"指:"#1"常量在常量池中。这个#1号常量就是"Method #6.#15;"(这里就很明显看出invokespecial是在调用java.lang.Object的初始化方法)。利用这些数字,我们就能不断的取得不可变的常量(final constant)。
而LineNumberTable为编译器提供了一些信息去指明哪行java源代码对应哪个字节码指令。例如,在java源代码中的第9行在main方法中对应的字节码为0,而第10行代码则对应字节码8。
如果你想了解更多的字节码,那你应该建立一个复杂点的类来编译它,HelloWorld毕竟只能作为说明而已。
4.它在JVM中是如何执行的?
现在来换个角度,问题是:JVM是如何加载类和调用类的main方法的呢?
在main方法执行之前,JVM需要加载,链接,初始化一个类。
a.加载是将类或者接口转换为二进制并且存入JVM中。
链接则将会把二进制数据包含至运行状态的JVM中。
b.链接分为三步:验证、准备、解析。
验证要确保类或者接口的内部结构的正确性;
准备则为类或接口分配所需的内存;
解析是将二进制中的符号替换成直接引用。
c.初始化则将会为类中的变量分配适当的初始值。
加载工作是由java的加载器执行的。当JVM开始时,三种类加载器将会被使用:
1.根加载器:加载java的核心库,核心库位于/jre/lib目录下,它是JVM的核心的一部分,并且是用的本地代码。
2.扩展加载器:加载外部jar包,用以扩展核心库。加载目录为/jre/lib/ext
3.系统加载器:加载CLASSPATH环境变量中能找到的所有类。
所以,HelloWorld类是被系统加载器加载的。当main函数被执行时,它将触发加载,链接,初始化以及其他与类操作相关的动作。
最后,main()将会被push至JVM栈中,程序计数器PC也将会被设置。紧接着PC指明需要将println()加入JVM栈中。当main方法完成后,它将弹出栈顶元素,函数的执行也就此完毕。