初学Java,大都会用记事本写个Hello Word,然后用CMD编译出class文件,最后执行一下。当控制台输出Hello Word的时候,一个简单的java入门demo就完成。然而从编写到输出,这中间虚拟机做了些?本文的基础是基于jdk7以及orcal的虚拟机。
为方便下文说明,这里对Hello Word做了下小小的更改。见如下代码:
public class HelloWord {
public static int i = 100;
private static final int L = 100;
public String sayHello() {
this.add(i, L);
return "hello word!";
}
public int add(int i, int l) {
return i + l;
}
public static void main(String[] args) {
System.out.println(new HelloWord().sayHello());
}
}
把他命名为HelloWord.java,现在打开cmd,使用javac编译。
得到一个HelloWord.class文件,这个class文件就是编译后的字节码文件,也是虚拟机只认的文件。为什么说虚拟机而不说Java虚拟机,因为虚拟机跟.java文件并没有关系,虚拟机只跟.class有关系,而能够编译成class文件的语言,不止java一种。
只要是符合虚拟机规范定义的class格式的字节码文件都是可以被虚拟机处理执行的,如果想要看class格式,jdk提供了一个工具javap,使用cmd定位到刚刚生成class文件目录,执行:
javap -verbose HelloWord.class
将会得到以下结果:
Classfile /D:/github/base/threadtest/bin/study/HelloWord.class
Last modified 2016-5-17; size 914 bytes
MD5 checksum b9b6ce77a3f846ce9dbf39183a5796c7
Compiled from "HelloWord.java"
public class study.HelloWord
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // study/HelloWord
#2 = Utf8 study/HelloWord
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 i
#6 = Utf8 I
#7 = Utf8 L
#8 = Utf8 ConstantValue
#9 = Integer 100
#10 = Utf8 <clinit>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Fieldref #1.#14 // study/HelloWord.i:I
#14 = NameAndType #5:#6 // i:I
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 <init>
#18 = Methodref #3.#19 // java/lang/Object."<init>":()V
#19 = NameAndType #17:#11 // "<init>":()V
#20 = Utf8 this
#21 = Utf8 Lstudy/HelloWord;
#22 = Utf8 sayHello
#23 = Utf8 ()Ljava/lang/String;
#24 = Methodref #1.#25 // study/HelloWord.add:(II)I
#25 = NameAndType #26:#27 // add:(II)I
#26 = Utf8 add
#27 = Utf8 (II)I
#28 = String #29 // hello word!
#29 = Utf8 hello word!
#30 = Utf8 l
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Fieldref #34.#36 // java/lang/System.out:Ljava/io/PrintStream;
#34 = Class #35 // java/lang/System
#35 = Utf8 java/lang/System
#36 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Methodref #1.#19 // study/HelloWord."<init>":()V
#40 = Methodref #1.#41 // study/HelloWord.sayHello:()Ljava/lang/String;
#41 = NameAndType #22:#23 // sayHello:()Ljava/lang/String;
#42 = Methodref #43.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#43 = Class #44 // java/io/PrintStream
#44 = Utf8 java/io/PrintStream
#45 = NameAndType #46:#47 // println:(Ljava/lang/String;)V
#46 = Utf8 println
#47 = Utf8 (Ljava/lang/String;)V
#48 = Utf8 args
#49 = Utf8 [Ljava/lang/String;
#50 = Utf8 SourceFile
#51 = Utf8 HelloWord.java
{
public static int i;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #13 // Field i:I
5: return
LineNumberTable:
line 20: 0
line 21: 5
LocalVariableTable:
Start Length Slot Name Signature
public study.HelloWord();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #18 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 19: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lstudy/HelloWord;
public java.lang.String sayHello();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: getstatic #13 // Field i:I
4: bipush 100
6: invokevirtual #24 // Method add:(II)I
9: pop
10: ldc #28 // String hello word!
12: areturn
LineNumberTable:
line 24: 0
line 25: 10
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lstudy/HelloWord;
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 29: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this Lstudy/HelloWord;
0 4 1 i I
0 4 2 l I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #33 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #1 // class study/HelloWord
6: dup
7: invokespecial #39 // Method "<init>":()V
10: invokevirtual #40 // Method sayHello:()Ljava/lang/String;
13: invokevirtual #42 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return
LineNumberTable:
line 33: 0
line 34: 16
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 args [Ljava/lang/String;
}
SourceFile: "HelloWord.java"
以下挑一些比较重要的属性进行介绍:
minor version:编译副版本号
najor version:编译主版本号,51代表是jdk7
Constant pool:常量池,这个里面存放的是类的一些基本信息、类变量以及引用符号等
再往下就是具体方法的编译,取一个sayHello方法为例:
descriptor:引用符号
flags:方法访问权限标志
code:方法体编译后的字节码指令
stack:预计占用的栈帧深度(后面会介绍)
locals:预计占用的空间内容
args_size:参数个数,实例方法默认会把this作为第一个参数,静态方法不会
LineNumberTable:对应到源文件中的行号
LocalVariableTable:局部变量表
不用理会那些字节码做什么的,只要知道那就是你写的代码编译后的样子就好了,其实有所了解的应该知道那属于栈集指令。
现在,我们已经做到了java到class的过程,并且看到class是如果编译的。下一步就是运行他,需要用到类加载器。
类加载器遵循双亲委派模型,当类加载器加载一个类的时候不会立即去加载,而是先让父级类加载器加载,这样逐级加载,只有当父级无法加载的时候才会自行加载。一般在java中有启动类加载器、扩展类加载器、应用程序类加载器以及自定义加载器。
类加载器加载HelloWord.class到虚拟机,就开始了class文件在虚拟机的流程,下图是一个class文件在虚拟机的生命周期:
加载
1、获取二进制流
2、转化成方法区运行时数据结构
3、在堆中生成对象
验证
1、文件格式验证:二进制流是否符合class规范,并且能被当前虚拟机能够的版本之内 (类)
2、元数据验证:分析字节码,保证其描述的信息符合java语言规范(类)
3、字节码验证:进行数据流和控制流分析(方法体)
4、符号引用验证:验证该类引用的其他类,字段,方法是否符合规范(类外引用的类, 字段,方法)
准备
方法区内为类变量,即被static修饰的变量,分配内存并设置类变量初始值,并非代码中设置的初始值,而是最原始的值,final修饰的常量除外.
解析
1、类或接口的解析
2、字段解析
3、类方法解析
4、接口方法解析
初始化
1、收集类中类变量的赋值动作、静态语句块合并产生<clinit>
2、先执行父类的<clinit>
3、若类中没有类变量和静态语句快则不生成<clinit>
4、<clinit>是同步的,同一时刻只会有一个线程能够执行,其他要使用这个<clinit>的线程等待
了解了以上知识,现在我们来看System.out.println(new HelloWord().sayHello());这句话,首先创建一个对象,然后调用方法,方法首先又调用了一个方法,最后返回一个字符串,输出。
在虚拟机是如何进行这一步操作呢?虚拟机数据模型分为pc寄存器、栈、本地方法栈、堆、方法区、常量池等。类被解析到方法区,对象被初始化到堆,常量放到常量池,而运行则在栈中进行,这里一个不得不引入一个概念-栈帧。
栈帧用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。村输了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成额过程,就对应着一个栈帧的虚拟机栈里面从入栈到出栈的过程,每个栈帧占用的内容空间和栈深度都在在编译期定义好的(code属性下的locals和stack)。
局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
操作数栈(后入先出)
方法执行的时候,刚开始是空的,经过指令向其执行入栈和出栈
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
方法返回地址
方法执行后有两种方式退出这个方法。第一个是执行引擎遇到任意一个方法返回的字节码指令,另一个是执行中遇到异常。无论哪种退出,都需要返回到方法被调用的位置,程序才能继续执行。
看到这就不难理解为什么sayHello的stack为什么是3了,这里虽然不涉及但是应该补充的知识点如下:
1、java是采用栈指令集,不依赖于寄存器,相比于寄存器指令集,其代码量多,运行速度相对慢,但可移植性大不受硬件限制。
2、slot:局部变量空间
3、java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量与java变成中的变量略有区别,他包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享。
4、java内存原型的特性:原子性、可见性、有序性。
原子性:read、load、assign、user、store、write操作原子性
可见性:volatile保证数据使用前从主内存获取数据和更改后立即刷新回主内存、synchronized对应lock和unlock,对一个变量执行unlock之前,必须先把此变量同步回主内存中、final修饰的数据,一旦被初始化完成,那么其他线程就能看见final的值。
有序性:如果在本县城内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有的操作都是无序的。
5、先行发生原则:
程序次序规则:同一线程内按照程序代码流程顺序先行发生(执行)
管城锁定规则:同一个锁的unlock先行发生lock之前(时间上)
volatile变量规则:valotile变量写操作先行发生读操作(时间上)
线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
线程终止规则:线程中的所有操作都先行发生于此线程的终止检测。
线程中断规则:对线程interrupt方法的调用先行发生于被终端县城的代码检测到中断事件的发生。
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize方法的开始
传递性:如果操作a先行发生于操作b,操作b先行发生于操作c,那就可以得出操作a先行发生于操作c的结论
volatile使用原则:对修饰的变量的修改不依赖变量的原值。
到这,一个新鲜的hello word就送到读者的面前,请尽情享用吧。
(本文图片来源于网络,限于时间以及个人能力,若有疏忽纰漏欢迎斧正)