概述
当我们执行java MathExample命令运行java程序时,JVM的"类加载子系统"会从classpath找到MathExample.class字节码,加载到"方法区(Method Area)",并且在"堆(Heap)"生成MathExample对象,接下来执行MathExample对象的main方法。详细的"类加载"过程,参考文章:类的生命周期。
接下快速回顾下大家都比较熟知的"运行时数据区"
运行时数据区
程序计数器(Program Couter Register)
程序计数器的作用是,CPU把当前线程挂起(即切换上下文)时,记录当前代码执行到哪一行。程序计数器由字节码执行引擎修改。程序计数器为每个栈帧开辟一块内存空间,存放每个方法运行到哪一行。要讲清楚程序计数器是如何工作的,需要讲一下字节码的执行原理。
字节码的执行原理
编译后的字节码在没有经过JIT(实时编译器)编译前,是通过字节码解释器进行解释执行。其执行原理为:字节码解释器读取内存中的字节码,按照顺序读取字节码指令,读取一个指令就将其翻译成固定的操作,根据这些操作进行分支,循环,跳转等动作。从字节码的执行原理来看,单线程的情况下程序计数器是可有可无的。因为即使没有程序计数器的情况下,程序会按照指令顺序执行下去,即使遇到了分支跳转这样的流程也会按照跳转到指定的指令处继续顺序执行下去,是完全能够保证执行顺序的。但是现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器
虚拟机栈(VM Stack)
每一个线程,都会在"虚拟机栈(VM Stack)"开辟一块独立的内存区。线程内部每个方法被执行的时候都会同时创建一个"栈帧(Stack Frame)",用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 局部变量表: 是用来存放 方法内的"局部变量"
- 操作数栈: 是用来存放 准备进入CPU的操作数。
- 动态链接: 将这些以符号引用所表示的方法转换为对实际方法的直接引用。类加载的过程中,将要解析尚未解析的符号引用,并且将对变量的访问转化为变量在程序运行时,位于存储结构的正确偏移量。
- 方法出口:承担恢复调用者状态的责任,包括恢复调用者的局部变量表和操作数栈,以及正确递增程序计数器,以跳过刚才执行的方法调用指令等。
堆(Heap)
堆(Heap)是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
方法区(Method Area)
方法区(Method Area)与堆(Heap)一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
本地方法栈(Native Method Stack)
本地方法栈是为虚拟机使用到的Native方法服务。
一个简单的java程序
接下来,我们通过一个简单的java程序在JVM层面的执行全过程,方便读者整体地了解上面提到的JVM知识。
public class MathExample {
private static final int num = 10;
public static void main(String[] args) {
MathExample mathExample = new MathExample();
mathExample.compute();
System.out.println("finish");
}
public int compute(){//一个方法对应一个栈帧内存区域
int a = 1 ;
int b = 2 ;
int c = (a+b) * num ;
return c;
}
}
该java程序执行全过程如下图所示:
如上图所示,图中的1-4步骤,对应着"类加载"的过程,可以参考文章:类的生命周期。
接下来重点探讨执行MathExample对象的main方法的过程。
JVM为了实现"跨平台",编译后的字节码不是直接映射为机器指令,而是映射为"JVM指令"。通过javap命令可以查看class文件所映射的"JVM指令"。为了方便查看,重新贴下main方法的代码
MathExample mathExample = new MathExample();
mathExample.compute();
System.out.println("finish");
再看main方法映射的"JVM指令":
public static void main(java.lang.String[]);
Code:
0: new #2 // class language/basic/tool/MathExample // 5.1 创建一个对象
3: dup // 5.2 复制操作数栈栈顶的值,并插入到栈顶
4: invokespecial #3 // Method "<init>":()V // 5.3 调用实例方法,专门用来调用父类方法、私有方法和实例初始化方法
7: astore_1 // 5.4 将一个reference类型的数据保存到本地变量表中
8: aload_1 // 5.5 从局部变量表加载一个reference类型值到操作数栈中
9: invokevirtual #4 // Method compute:()I // 5.6 调用实例方法,依据实例的类型进行分派
12: pop // 将操作数栈的栈顶元素出栈
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; //获取类的静态字段值
16: ldc #6 // String finish // 从运行时常量池中提取数据并压入操作数栈."finish"是运行时常量。
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V //调用实例方法,依据实例的类型进行分派
21: return //方法中返回void
三行java代码,JVM生成了11条"JVM指令"。每条指令集的意思,我已经在后面写好注释了。大家也可以在网上搜索下"JVM指令集",找到所有"JVM指令"对应的含义。
main方法调用了compute方法,从上述的"JVM指令"可以看到,通过invokevirtual指令进行。接下来,我们继续分析compute方法的执行过程。
int a = 1 ;
int b = 2 ;
int c = (a+b) * num ;
return c;
再看compute方法映射的"JVM指令":
public int compute();
Code:
0: iconst_1 // 6.1 将int类型常量(1)入栈到操作数栈中
1: istore_1 // 6.1 将操作数栈中的一个int类型数据保存到局部变量表中,即局部变量1.
2: iconst_2 // 6.2 将int类型常量(2)入栈到操作数栈中
3: istore_2 // 6.2 将操作数栈中的一个int类型数据保存到局部变量表中,即局部变量1.
4: iload_1 // 6.3 把"局部变量1"(即a)加载到"操作数栈"
5: iload_2 // 6.3 把"局部变量2"(即b)加载到"操作数栈"
6: iadd // 6.4 int 类型数据相加. 让JVM的执行引擎对"操作数栈"的数据放到CPU里执行加法,得到结果压回"操作数栈"。
7: bipush 10 // 将立即数byte带符号扩展为一个int类型的值value,然后将value入栈到操作数栈中。即把常量10放到操作数栈中
9: imul // int类型数据乘法,执行引擎运行出结果后返回到操作数栈中。
10: istore_3 // 6.5 将操作数栈中的一个int类型数据保存到局部变量表中,即局部变量3.(即c)
11: iload_3 // 把"局部变量3"(即c)加载到"操作数栈"
12: ireturn // 从方法中返回一个int类型数据
注意:iload指令是把"局部变量1"(即a)加载到"操作数栈",然后JVM会把"操作数栈"的数据加载到寄存器中。iadd指令,JVM会让CPU对寄存器里的值进行相加。
Q&A:
1. "JVM指令" 怎样做到跨平台?
不同厂商的机器对应着不同的"机器指令集"。java解决的思路是,先把所有java生成 "JVM指令",然后"JVM指令"再根据不同的操作系统以及CPU架构选择不同的"机器指令集"。选择的步骤,在我们下载JDK的时候决定。这样JVM就屏蔽了不同硬件的差异性,从而达到"跨平台"的目的。
2. 什么叫立即数?
通常把在立即寻址方式指令中给出的数称为立即数。立即数可以是8位、16位或32位,该数值紧跟在操作码之后。
3. JVM如何在方法调用时传递this指针?
默认每个方法都有 iconst_0 ,代表this指针。
我一直有个疑问:为什么 程序计数器 要 单独一块内存,而不是放到虚拟机栈(VM Stack)里面?知道答案的读者,欢迎留言给我。
附录
上述程序通过javac(java编译器)生成MathExample.class,通过 jdk自带的反解析工具javap 查看class对应的完整"Java虚拟机指令集" 。
$ javap -c MathExample.class
Compiled from "MathExample.java"
public class language.basic.tool.MathExample {
public language.basic.tool.MathExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class language/basic/tool/MathExample
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #6 // String finish
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
public int compute();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
}
最后
以上就是正直跳跳糖为你收集整理的java程序执行全过程运行时数据区一个简单的java程序的全部内容,希望文章能够帮你解决java程序执行全过程运行时数据区一个简单的java程序所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复