• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

深入分析Java的HelloWorld程序

Java高级 tiantian 1860次浏览 0个评论 扫描二维码

在IT界,学习任何一门新技术都是从 HelloWorld 程序开始的。对于Java中的HelloWorld程序,我相信每个Java程序员都不会不熟悉。它非常地简单,但就是这个简单的程序代码背后隐藏着很多比较复杂的概念和原理。本文将深入分析Java中的HelloWorld程序,看看能挖掘出什么有深度的知识。

HelloWorld

public class HelloWorld {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Hello World");
}
}

一切从class开始

Java应用都是使用类(class)建造的,每一个方法或者属性都必须位于某一个类(class)中。这是由Java语言的面向对象特性决定:Java应用的一切都是对象,每个对象都是一个类的实例。与过程式的编程语言相比,面向对象的编程语言具有许多的优势,比如封装性、模块化和可扩展等。

永远的main()方法

Java应用的唯一入口就是某个类中的 mian() 方法,它还是一个静态方法。静态特性意味着 main() 方法是类(class) 的一部分,而不是对象的一部分。为什么是这个样子呢?为什么要使用一个静态的方法作为Java应用的入口呢?

如果一个方法不是静态的,那么在使用该方法之前,必须先创建该方法所在类的对象,这是因为非静态的方法必须在该类的示例对象上调用。对于应用的入口而言,非静态的方法就显得很不灵活和方便了。所以,Java应用入口使用了更方便的静态方法。

在 main() 方法中,参数类型是 “String[] args”, 意味着在启动Java应用的同时,传递一个 String 类型的数组来定制化应用的初始化属性。

深入字节码(Bytecode)

众所周知,Java是一门结合了编译性和解释性的语言。为了可以执行Java程序,需要使用Java编译器把Java源码文件(.java)编译成Java的字节码(Bytecode)文件(.class)。那么 HelloWorld.java 的字节码看起来是怎么样的?如果我们使用一个具有十六进制功能的编辑器打开 HelloWorld.class。效果如下图所示:

深入分析Java的HelloWorld程序

我们可以从上图中看到很多的操作码(opcode)(比如 CA,4C,等),而且,每一个操作码都有一个对应的助记符(mnemonic)(比如 aload_0 等)。由于操作码的可读性很不好,所以,我们可以使用 javap 指令来查看助记符形式的 .class文件。

&emps;我们知道,”javap -c”可以打印出类中方法的未组装版本,为组装版本的意思就指:只简单地翻译字节码到助记符形式。假设我们执行如下所示的指令:

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."":()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规范中的 Java bytecode instruction listings。我们看几个常见的指令,aload_0 完成从 装载一个引用值到局部变量表的0号位置;getstatic 负责抓取类的某个静态成员,而其后的 “#2″指向了运行时常量池中的具体位置。运行时常量池是JVM运行时方法区中的某一个部分区域。如果希望查看一下JVM运行时常量池的情况,我们可以使用 “javap -verbose”指令。

另外,每一条指令的前面都有一个数字,比如 0,1,4 等。在 .class 文件中,每个方法都有一个对象的字节码数组(bytecode array)。这些数字就对应该数组中的索引位置,数组里存储着操作码和操作数。每一个操作码的空间都是一个字节,同时,每个操作码可以有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."":()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    ;
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;//  "":()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."":()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
}

在Java规范中,运行时常量池的作用非常类似于传统编程语言中的符号表,当然,它包含了比传统符号表更多的数据和信息,而且还可以动态扩展和收缩。”invokespecial #1″ 指令中的 “#1” 就指向了常量池中的1号常量位置。这个位置的常量是 “Method #6.#15;” 。如果继续探寻下去,我们可以获得最终的完整常量值。

行号表(LineNumberTable)为调试器提供了有效的信息,可以用来在Java源码文件与Java字节码文件之间建立起准确地行数对应信息。举例而言,Java源码文件中的第九行对应到Java字节码文件中main方法的的第0行。如果你有继续深入了解字节码的欲望,你可以编写一个更加复杂的测试代码,利用指令查看细节的过程中并对照Java规范。

执行过程

现在的问题是:JVM是如何加载类,又是如何调用 main() 方法的?我们简单地总结一下整个流程:
1. load(装载):把编译生成的 .class 文件读入到JVM中,存放在内存中。
2. link(链接):合并读入内存的 .class 文件,解析文件之间的引用关系。该过程进一步分成:verification(验证),preparation(预处理),可选的resolution(解析)。验证确保读入的 .class 文件是符合规范标准。预处理主要的作用是分配存储所需的内存空间。解析是负责解析 .class 文件之中的符号引用。
3. initialize the class(初始化类):把类(class)的变量初始化到一个合适的状态。
4. executed the main method(执行main方法)

深入分析Java的HelloWorld程序

实际上,装载的工作是由Java的 类加载器 完成的。当JVM启动的时候,会同时启动三个类加载器。
1. Bootstrap class loader(根加载器):负责加载位于 “/jre/lib” 目录下的核心Java类库。该根加载器是JVM核心的一部分,而且是使用本地语言(C/C++)实现的。
2. Extensions class loader(扩展加载器):负责加载位于 “/jar/lib/ext” 等目录下的扩展Java类库。
3. System class loader(系统加载器):负责加载位于 CLASSPATH 路径中的Java类。

所以,我们的 HelloWorld 类是被系统加载器加载的。在该类的 main() 方法被执行之前,它会首先完成 装载、链接、初始化其他被依赖类等工作。

最后,包含 main() 方法的栈帧会被推入到JVM的 “mian” 线程所在的栈中,同时,程序计数器也已经被设置妥当。随后,被调用的 println() 方法又会生成一个栈帧被推入到 “main” 线程所在的栈中。当 main() 方法执行完毕,整个的栈会被销毁,整个的应用也就正常结束了。


天天编码 , 版权所有丨本文标题:深入分析Java的HelloWorld程序
转载请保留页面地址:http://www.tiantianbianma.com/java-helloworld.html/
喜欢 (6)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址