朋友们大家好,我是加瓦老狗,从今天开始我将在我自己的平台上分享各种Java技术,旨在跟各位同行交流分享,共同学习进步,如果有不正确的地方,欢迎提出,非常感谢。
本文作为我的第一篇技术分享,从一道Java面试题说起:Java代码的执行过程是怎样的?
首先我们在JDK安装包bin目录下可以看到有两个程序javac.exe和java.exe,这两个程序代表了Java代码执行的两个阶段,编译和运行
编译阶段
上图所示是Java代码执行的编译阶段,程序员编写的.java文件通过javac指令(编译器)校验完语法正确性后生成.class文件即字节码文件
编译阶段并不复杂,复杂的是运行阶段,在说运行阶段之前,先来了解下JVM
JVM
JVM,也就是Java虚拟机。针对不同的操作系统,JVM有不同的实现。JVM将编译生成的.class解析成相应操作系统可识别的机器码(即二进制),在操作系统上运行,这也是Java语言可跨平台的原因。
JVM是JDK的一部分,我们来看一下JVM里面有什么,包含了哪几部分。
拿最经典的JDK8来说,在oracle官网,JVM规范里,JVM运行时数据区有六部分:程序计数器、虚拟机栈、堆、方法区(元空间,JDK1.8及以后)、运行时常量池、本地方方法栈
-
The pc Register(程序计数器):存的是下一个要执行的字节码指令
什么是字节码指令呢?
.java源文件编译成.class文件后,在.class文件目录用javap -c命令可以查看字节码命令,就拿最简单的Hello World!代码举例
public class Main { public static void main(String[] args) { System.out.println("Hello world!"); } }
如图方框内的就是字节码指令
-
Java Virtual Machine Stacks(Java虚拟机栈):用于存储栈帧。栈帧用于存储方法调用时的局部变量表,操作数栈等,每一个方法调用都会对应一个栈帧
上述Hello World!代码方法中:
-
getstatic #7
:-
getstatic
指令用于获取一个静态字段的引用。 -
#7
是该指令的属性索引,指向常量池中的一个常量。 -
这个常量表示
java/lang/System
类的out
字段,这是一个静态字段,表示标准输出流。 -
这条指令将
System.out
的引用加载到操作数栈中。
-
-
ldc #13
:-
ldc
指令用于加载一个常量值。 -
#13
是该指令的属性索引,指向常量池中的一个字符串常量。 -
这个字符串常量是
"Hello world!"
。 -
这条指令将字符串常量加载到操作数栈中。
-
-
invokevirtual #15
:-
invokevirtual
指令用于调用一个实例方法。 -
#15
是该指令的属性索引,指向常量池中的一个方法引用。 -
这个方法引用表示
java/io/PrintStream
类的println
方法,用于打印一个字符串并换行。 -
这条指令执行
System.out.println("Hello world!")
操作。
-
-
return
:-
return
指令用于结束当前方法并返回。 -
在
main
方法中,它表示程序的结束。
-
-
-
Heap(堆):是JVM中最大的一块内存,用于存储Java对象实例以及数组等,JDK8及之后静态变量、常量、字符串常量池也在堆内存。堆是垃圾收集器收集垃圾的主要区域。被所有线程共享
-
Method Area(方法区/元空间):存储已被虚拟机加载的类信息等
-
Run-Time Constant Pool(运行时常量池):方法区的一部分,用于存储编译期生成的各种字面量与符号引用
-
Native Method Stacks(本地方法栈):和虚拟机栈类似,只是服务于本地方法,带有Native修饰的方法(底层C++实现)
以上这些运行时数据区在JVM启动时都需要被创建,且在虚拟机运行期间始终存在,直到JVM停止运行时被销毁
运行阶段
类加载
类加载是指类从被加载到JVM开始,到被卸载出JVM的过程,类加载分为加载、连接、初始化三个过程
加载
.class字节码文件经过java命令后,JVM就启动了,进入了运行阶段,首先经过类加载器,类加载器会根据CLASSPATH环境变量的路径去寻找class文件,将class文件装载到JVM中,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(自己不回去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上)
加载class文件到JVM中,在这个过程,JVM需要完成以下几件事
-
通过一个类的全限定类名来获取此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在JVM堆中创建一个java.lang.Class类的对象,作为方法区这个类的各个数据的访问入口,并将类相关的信息存储在JVM方法区中
连接
连接又分为三个阶段验证、准备、解析
-
验证:验证类是否符合Java规范和JVM规范,防止恶意代码的执行,相当于是一次DoubleCheck
-
准备:JVM为类的静态变量分配内存空间,将其初始化为默认值
-
解析:JVM将符号引用转化为直接引用,即将类、方法、字段等符号引用转换为内存中的直接地址
初始化
JVM执行类的静态代码块,对静态变量进行初始化,从上往下执行
解释
初始化完成后,当我们要执行一个类的方法时,会找到对应方法的字节码信息,然后解释器会把字节码信息解释成操作系统能识别的机器码。
在JVM中有字节码解释器和即时编译器(JIT)。在解释时会对代码进行分析,是否为“热点代码”,当某个方法或代码块运行的很频繁的时候,JVM会把这部分代码认定为“热点代码”。如果是“热点代码”则直接出发JIT编译,JIT把热点方法的指令码保存起来,下次执行时无需重复进行解释,直接执行缓存的机器语言提高解释速度。这也是代码多次执行后会变快的原因
执行
操作系统将解释出来的机器码,调用系统的硬件执行
总结
Java代码执行的过程,可以总结为四个步骤:编译、类加载三步(加载、连接、初始化)、解释、执行
编译:经过语法检验后生成class文件
类加载:加载->连接->初始化。加载是把class文件装载到JVM,连接则校验class信息,分配内存空间初始化默认值,初始化是对静态变量和代码块进行初始化
解释:把字节码转换成操作系统可识别的执行指令
执行:调用系统的硬件执行最终的程序指令