目录
1.解释器与编译器
上文《JVM读书笔记-1.来福与旺财的养牛场》讲述了一个简单易懂但不确切的故事来描述解释器与编译器。那解释器和编译器到底是什么?
解释器:将程序源码一条一条地解释执行
编译器:将程序源码整个编译成目标代码,执行过程中不在需要编译器,直接在支持目标代码的平台上运行。
程序的执行是首先将目标源代码编译/解释成运行平台上的机器码或者虚拟机所支持的指令集,然后再运行平台上执行。
1.1编译执行
比如在Windows上C++程序通过编译器将源代码编译目标物理机(x86/ARM/MIPS)上的机器码,然后再目标机上运行
一个萝卜一个坑,编译器将源代码编译成目标平台所支持的机器码,然后目标平台可以直接运行。这样秦光霞,程序运行效率较高。
1.2解释执行
解释器是“边解释,边运行”。
解释器运行比较简单,一般程序直接运行就可以,不需要编译步骤。不过每次代码在解释器中执行时会被编译多次,相对开销比较大,效率也相对低。
1.3半编译半解释执行
Java以及C#采用的是这种方式。首先将源代码编译成中间代码,然后再在解释器中动态编译执行。
Java最初是通过解释器解释执行的,在目标平台上解释执行预编译的中间代码,执行效率相对较低。当程序运行一段时间后,JVM通过热点代码检测定位程序的“热点代码”,并通过即时编译进行动态编译与优化,使得Java程序的运行速度随着运行时间的增加而获取更高的性能。Java是一门半解释半编译的语言。
2.编写源码
通过一个简单的Java源代码讲述Java程序从创建到编译直到运行的过程。
// Test.java
public class Test {
public static void main(String[] args) {
Student student = new Student();
student.learning();
}
}
//Student.java
public class Student {
public void learning() {
System.out.println("JVM Learning...");
}
}
3.前端编译
Java的编译过程分为前端编译(Javac)以及后端即时编译(JIT)。这里按照程序运行过程先讨论Java的前端编译过程。
前端编译器对源码进行词法以及语法分析形成抽象语法树并生成最终可执行的程序。
前文已经提到Java/C#等语言是将源代码通过编译器预编译成中间代码,然后通过解释器在目标平台上运行。Java源代码的这个预编译过程是通过前端编译器(Javac编译\ECJ增强式编译器等)完成的。
使用javac编译Test.java以及Student.java两个源文件生成两个后缀名为.class的两个字节码文件。
字节码文件使得Java程序摆脱了硬件平台的束缚,实现了Java“一次编写,处处运行”的核心思想。另外,JVM运行的是class类型的文件,只要是符合class类型文件要求的都可以运行,为混合语言的使用提供了基础。
通过java自带的工具javap来查看编译生成的class文件,比如上文中重定向出来的的test.txt。打开这个文件,我们可以看到class文件主要包括两块内容:常量池以及方法字节码。
常量池(Constant pool):
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Student
#3 = Methodref #2.#15 // Student."<init>":()V
#4 = Methodref #2.#17 // Student.learning:()V
#5 = Class #18 // Test
#6 = Class #19 // 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 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 Student
#17 = NameAndType #20:#8 // learning:()V
#18 = Utf8 Test
#19 = Utf8 java/lang/Object
#20 = Utf8 learning
方法字节码:
public Test();
descriptor: ()V
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[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Student
3: dup
4: invokespecial #3 // Method Student."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Student.learning:()V
12: return
LineNumberTable:
line 4: 0
line 5: 8
line 6: 12
class文件记录了类的字段、方法、父类以及实现的接口等信息。
4.类的加载
通过javac编译后的java类可以通过 "java 类名"运行,java类的运行包含两个步骤:类的加载与运行。类的加载过程是通过ClassLoader类完成的。类的加载机制分为加载、链接以及初始化过程。
4.1加载
程序运行前首先要将预编译生成的.class文件的字节码加载到内存中。class文件的加载是通过ClassLoader类完成的。ClassLoader将这些静态数据加载到内存中,转换为JMM模型中方法区的运行时数据,并在堆中生成一个代表该类的class对象,该对象作为方法区数据的访问入口,这个过程需要类加载器的参与。
4.2链接
链接是将java类的二进制代码合并到JVM运行状态之中的过程
a.验证:确保加载的类信息符合JVM规范,没有安全方面的问题
b.准备:正式为类变量分配内存并设置变量初始值的阶段,这些内存在方法区进行分配
c.解析:虚拟机常量池的符号引用替换为字符引用的过程
4.3初始化
初始化是执行类构造器<cliinit>()方法的过程。
a.类构造器<Clinit>()方法是由类编辑器自动收藏类中所有类变量的赋值动作和静态语句块合并产生
b.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先出发其父类的初始化
c.虚拟机会保证一个类<clinit>()方法在多线程环境中被正确加锁和同步
d.当范围一个Java类的静态域时,只有真正声明这个域的类才会被初始化。
参考文献
2.《浅谈对JIT编译器的理解》