public class HelloWorld { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("Hello World"); } }
java程序是由类组成的,每一个方法和字段都存在于类中。这是因为其面向对象的特色所决定的:任何东西都是一个类实例的对象。较之结构化程序语言来讲,面向对象的语言有许多优势,如模块化、可扩展性等等。html
main方法是一个程序的入口,而且它是静态的。所谓静态,指这个方法是属于类的,而不是某个对象的。java
那又是为何?为何咱们不将一个非静态的方法作为程序的入口呢?编程
若是一个方法是非静态的,那么为了使用这个方法,咱们必须得去创建一个对象,由于非静态的方法必须由对象来调用。对于入口来讲,这显然是不现实的。所以,程序的入口方法是静态的。数组
参数"String[] args"则代表有一个字符串的数组参数将会关联至程序中,帮助程序进行初始化。oracle
执行这个程序时,java文件(.java)首先将会被编译为java字节码,而且存储在.class文件中。jvm
那么这个字节码长的什么样子呢?编程语言
java字节码自己是不可读的,可是咱们若是用一个hex编辑器打开的话,它将呈现以下:编辑器
在上图中的字节码里,咱们能看到有许多操做码(CA,4C等等),它们各自都有本身相关的记忆码(至关于助记符,以下面将会出现的aload_0).操做码是不可读的,可是咱们能够用javap指令来看看.class文件中的助记符究竟是什么样的。模块化
"javap -c"将会为每一个类中的方法打印出反汇编代码。反汇编代码指的是包含众多java字节码的指令集。函数
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."<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字节码指令表。例如,aload_0指的是从局部变量0加载一个引用至栈顶,getstatic则是获取一个类的静态字段。须要注意,在getstatic指令后的"#2"指的是运行时的常量池。常量池是JVM运行时的数据存储区域(JVM Run Time Data Areas)。利用"javap -verbose"命令将会让咱们看到常量池。
此外,每一个指令的开始都伴随一个数字,如0,1,4等。在.class文件中,每个方法都有一个相关的字节码数组。这些数字则对应着操做码和它的参数存储在字节码数组的索引位置。每一个操做码都是一个字节的长度,而且指令集能够有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."<init>":()Vconst #2 = Field #16.#17; // java/lang/System.out:Ljava/io/PrintStream;const #3 = String #18; // Hello Worldconst #4 = Method #19.#20; // java/io/PrintStream.println:(Ljava/lang/String;)Vconst #5 = class #21; // HelloWorldconst #6 = class #22; // java/lang/Objectconst #7 = Asciz <init>;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;// "<init>":()Vconst #16 = class #23; // java/lang/Systemconst #17 = NameAndType #24:#25;// out:Ljava/io/PrintStream;const #18 = Asciz Hello World;const #19 = class #26; // java/io/PrintStreamconst #20 = NameAndType #27:#28;// println:(Ljava/lang/String;)Vconst #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."<init>":()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}
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毕竟只能做为说明而已。
如今来换个角度,问题是:JVM是如何加载类和调用类的main方法的呢?
在main方法执行以前,JVM须要加载,连接,初始化一个类。
a.加载是将类或者接口转换为二进制而且存入JVM中。
连接则将会把二进制数据包含至运行状态的JVM中。
b.连接分为三步:验证、准备、解析。
验证要确保类或者接口的内部结构的正确性;
准备则为类或接口分配所需的内存;
解析是将二进制中的符号替换成直接引用。
c.初始化则将会为类中的变量分配适当的初始值。
Before the main method is executed, JVM needs to 1) load, 2) link, and 3) initialize the class. 1) Loading brings binary form for a class/interface into JVM. 2) Linking incorporates the binary type data into the run-time state of JVM. Linking consists of 3 steps: verification, preparation, and optional resolution. Verification ensures the class/interface is structurally correct; preparation involves allocating memory needed by the class/interface; resolution resolves symbolic references. And finally 3) initialization assigns the class variables with proper initial values.
加载工做是由java的加载器执行的。当JVM开始时,三种类加载器将会被使用:
1.根加载器(Bootstrap class loader):加载java的核心库,核心库位于/jre/lib目录下,它是JVM的核心的一部分,而且是用的本地代码。
2.扩展加载器(Extensions class loader:):加载外部jar包,用以扩展核心库。加载目录为/jre/lib/ext
3.系统加载器(System class loader):加载CLASSPATH环境变量中能找到的全部类。
Bootstrap class loader: loads the core Java libraries located in the
/jre/lib directory. It is a part of core JVM, and is written in native code.Extensions class loader: loads the code in the extension directories(e.g.,
/jar/lib/ext).System class loader: loads code found on CLASSPATH.
因此,HelloWorld类是被系统加载器加载的。当main函数被执行时,它将触发加载,连接,初始化以及其余与类操做相关的动做。
最后,main()将会被push至JVM栈中,程序计数器PC也将会被设置。紧接着PC指明须要将println()加入JVM栈中。当main方法完成后,它将弹出栈顶元素,函数的执行也就此完毕。