Java基础知识专题7-阶段总结-Java代码在JVM中执行过程实例详解
前言
经过前面几章,已经分块的将JVM的关键部分进行了讲解。本章是一个阶段性的回顾章节,将通过整体一个代码例子来讲Java程序从编写完成到进入虚拟机执行再到最后结束串起来,希望能够通过这个例子对JVM整体内容有更加深刻的理解。
图解回忆
开始实例之前,我们先通过前面几章的图,来回顾一下前几张的主要内容,同时也真题回顾一下Java代码的整个执行过程。
首先是Java整体结构:
上图中红色框的部分是最基础需要使用的:javac编译器、javap字节码查看器、lang&Util基础包、JVM。
然后是Java代码编写完成后的执行流程:
这个过程分为两个大阶段:编译阶段、运行阶段。
然后是编译阶段javac所做的工作:
这个过程我们编写的Java(.java)文件,通过编译器就编译成了JVM认识的字节码(.class)文件。
接下来编译好的字节码程序就交给JVM:
JVM通过经过类加载、内存分配、执行引擎执行完成程序的执行工作。
在执行的过程中,GC会随时进行垃圾回收:
样例代码
class Person
{
private String name;
private int age;
public Person(int age, String name){
this.age = age;
this.name = name;
}
public void run(){
}
}
interface IStudyable
{
public int study(int a, int b);
}
public class Student extends Person implements IStudyable
{
private static int cnt=5;
static{
cnt++;
}
private String sid;
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
public void run(){
System.out.println("run()...");
}
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
public static int getCnt(){
return cnt;
}
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}
}
准备:编译代码-从源代码到字节码
首先我们需要使用JDK中的编译器对我们的样例代码进行编译,让源代码成为字节码。这里由于比较简单,我就直接使用cmd进行演示,开发过程中我们肯定是使用IDE进行这个步骤。
第一步:在Java文件所在的目录下启动cmd:
第二步:在cmd执行,javac Student.java命令,显示完成查看文件目录,代码编译完成:
由于Student.java文件中我们不止声明了Student类,还声明了他的父类Person,以及接口IStudyable,所以编译后会生成三个class文件,这是应为编译器会自动拆分,每个类或接口都是一个单独的class文件。
然后我们使用javap指令查看一下编译好的字节码的内容:
这里有两条指令:
- javap Student.class:查看字节码的概要信息
- javap -v Student.class:查看字节码的详细内容
内容主要分为以下几部分:
- 第一部分:类的头信息,包括版本、继承关系、来源、加密信息等;
- 第二部分:常量池,包括这个类的方法引用、各种变量的声明、以及所继承的父类、实现的接口等等内容信息;
- 第三部分:就是这个类的描述代码描述,一个一个的方法体、代码块(逻辑部分)。
这一部分主要是要结合JVM指令集才能看懂,此处我不做过多解释,只需指导这些内容都是类加载阶段要放到方法区的,执行阶段会执行第三阶段的逻辑代码,使用第一二阶段的内容即可。
运行代码
代码的编译和JVM只是规则上的关系,还没有真正将代码交给JVM。接下来我们就要执行这段代码,JVM就要接手了。
还是在刚才的命令行执行:java Student,可以看到代码中的我们输出的内容打印出来了,同时出现了下条指令等待输入的标识,表示我们的代码已经执行完成了。
虽说执行这段代码我们只是简单的用了一个java命令,但是JVM在后面的处理是非常复杂的。
第二步:执行代码-从字节码到程序实现
当类加载完成,准备工作都做好了,接下来就要由执行引擎开始按照指令执行代码了。
在解释执行过程前,先要回顾两个概念:运行时栈及栈帧、JVM执行引擎。
运行时栈及栈帧
JVM会为每个线程在栈区创建一个线程栈,这个线程栈中以栈帧(前面章节已介绍)为基本元素。
栈帧是用于支持JVM进行方法调用和方法执行的数据结构,每个方法从调用到执行完成的过程,对应着一个栈帧在线程栈的入栈到出栈的过程。
JVM执行引擎
执行引擎以指令为单位读取Java字节码。它就像一个CPU一样,一条一条地执行机器指令。每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
执行引擎包含三个部分:解释器、JIT(Just-In-Time)编译器、垃圾收集器:
解释器:一条一条读取字节码(.class)文件中的指令,解释并执行这些指令。他的特点是解释字节码很快,但是执行会比较慢,因为机器执行机器码是很快的,而解释器则是要一条一条的按部就班的进行,所以导致整体速度受到影响。
JIT编译器:JIT对程序需要经常调用的代码(热点代码:Hot Spot Code,如循环、高频使用的方法等),为了提高运行效率,JVM就把这些代码编译成本地机器码,并且根据计算机特性进行各种层次优化。
JIT被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。因此,如果代码只被执行一次的话,那么最好还是解释执行而不是编译后再执行。因此,内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
垃圾收集器
负责JVM内存的管理,主要是对堆区的管理。
接下来就通过前面的样例代码看一下执行引擎和栈是如何配合完成执行一段代码的。
1、首先JVM启动后会为程序创建一个线程,同时为这个线程分配对应的栈和PC计数器;
2、找到main()方法,将没方法作为第一个栈帧,入栈;
根据指令:stack=5、locals=2分配局部变量表两个slot并放入参数args和局部变量s(Student),分配操作数栈5个slot。
只有当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶;当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。
注意:局部变量表及操作数栈的最大深度在编译期间就已经确定了,存储在该方法字节码的Code属性中。
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}
3、准备创建Student对象;
运行第一条指令:new #14 在Java对上创建Student对象;
运行第二条指令:将Student对象的引用推入操作数栈;
同时该线程的PC计数器要更新指令操作到第2条指令(从0开始)。
4、操作数入栈,准备调用创建Student对象;
运行第三条指令:bipush 23 将值23推入操作数栈;
运行第三条指令:ldc #8:将#8这个常量池中的常量即”dqrcsc”取出,并入栈;
运行第四条指令:ldc #9:将#9这个常量池中的常量即”20150723”取出,并入栈;
运行第五条指令:invokespecial #10:调用#10这个常量所代表的方法,即Student的构造方法。
5、调用Student构造方法,入栈新的栈帧;
运行第六条指令:invokespecial #17 // Method “”:(ILjava/lang/String;Ljava/lang/String;)V
向线程栈中推入Student构造方法的栈帧;
6、执行Student构造方法,填充对象内容(操作与main方法中的赋值等操作类似);
根据构造方法,将入参内容赋值给堆中对象的字段;
7、结束Student构造方法,返回main方法继续执行;
8、接下来的指令就是调用study方法、getCnt方法、run方法,与Student构造方法类似,都要入栈新的栈帧进行操作;
9、最后运行:
invokevirtual #13:调用0x2222对象的run()方法,重写自父类的方法,需要动态分派,所以使用invokevirtual指令
return:main()返回,程序运行结束。
第三步:垃圾回收-从内存到释放
在程序运行过程中,GC会根据内存的使用情况进行内存回收,此内容在GC章节已介绍,此处不赘述了。
结语
至此本章的内容就完成了,其中指令运行过程如果有兴趣可以再仔细研究,整体流程才是大家要熟悉的关键。
参考资料
https://www.cnblogs.com/dqrcsc/p/4671879.html