• 35648

    文章

  • 23

    评论

  • 20

    友链

  • 最近新加了很多技术文章,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

简易Java(01):从<code>HelloWorld</code>中可以学到什么?

欢迎来到阿八个人博客网站。本 阿八个人博客 网站提供最新的站长新闻,各种互联网资讯。 喜欢本站的朋友可以收藏本站,或者加QQ:我们大家一起来交流技术! URL链接:https://www.abboke.com/ITjs/2019/0611/675.html 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为扩展名的文件。

字节码长什么样子呢?

字节码本身是不易读。如果我们使用十六进制编辑器打开,则它看起来如下图所示:

HelloWorld字节码

在上面的字节码中,我们可以看到很多操作码(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."&lt;init&gt;":()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_0invokespecial #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."&lt;init&gt;":()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               &lt;init&gt;
   #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          //  "&lt;init&gt;":()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."&lt;init&gt;":()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)。校验确保这个类或者接口是结构正确的;准备涉及类或者接口所需内存的分配;解析是将符号应用替换为直接引用。最后,初始化是给变量赋予合适的初始化值。

Java虚拟机加载过程

加载的工作由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面试题的。感觉不错,所以就翻译过来分享一下。这是第一篇。

参考资料



相关文章

暂住......别动,不想说点什么吗?
  • 全部评论(0
    还没有评论,快来抢沙发吧!