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