在第7篇详细介绍过为Java方法创建的栈帧,如下图所示。

调用完generate_fixed_frame()函数后一些寄存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址

现在我们举一个例子,来完整的走一下解释执行的过程。这个例子如下:

package com.classloading;

public class Test {
public static void main(String[] args) {
int i = 0;
i = i++;
}
}

通过javap -verbose Test.class命令反编译后的字节码文件内容如下: 

Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/classloading/Test
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Test.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/classloading/Test
#14 = Utf8 java/lang/Object
{
... public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0
1: istore_1
2: return
}

如上实例对应的栈帧状态如下图所示。

现在我们就以解释执行的方式执行main()方法中的字节码。由于是从虚拟机调用过来的,而调用完generate_fixed_frame()函数后一些寄存器中保存的值并没有涉及到栈顶缓存,所以需要从iconst_0这个字节码指令的vtos入口进入,然后找到iconst_0这个字节码指令对应的机器指令片段。

现在回顾一下字节码分派的逻辑,在generate_normal_entry()函数中会调用generate_fixed_frame()函数为Java方法的执行生成对应的栈帧,接下来还会调用dispatch_next()函数执行Java方法的字节码,首次获取字节码时的汇编如下:

// 在generate_fixed_frame()方法中已经让%r13存储了bcp
movzbl 0x0(%r13),%ebx // %ebx中存储的是字节码的操作码 // $0x7ffff73ba4a0这个地址指向的是对应state状态下的一维数组,长度为256
movabs $0x7ffff73ba4a0,%r10 // 注意%r10中存储的是常量,根据计算公式%r10+%rbx*8来获取指向存储入口地址的地址,
// 通过*(%r10+%rbx*8)获取到入口地址,然后跳转到入口地址执行
jmpq *(%r10,%rbx,8)

注意如上的$0x7ffff73ba4a0这个常量值已经表示了栈顶缓存状态为vtos下的一维数组首地址。而在首次进行方法的字节码分派时,通过0x0(%r13)即可取出字节码对应的Opcode,使用这个Opcode可定位到iconst_0的入口地址。

%r10指向的是对应栈顶缓存状态state下的一维数组,长度为256,其中存储的值为Opcode,这在第8篇详细介绍过,示意图如下图所示。

现在就是看入口为vtos,出口为itos的iconst_0所要执行的汇编代码了,如下:

...

// vtos入口
mov $0x1,%eax ...
// iconst_0对应的汇编代码
xor %eax,%eax

汇编指令足够简单,最后将值存储到了%eax中,所以也就是栈顶缓存的出口状态为itos。

上图中绿色的部分是表达式栈,而紫色的部分是本地变量表,由于本地变量表的大小为2,所以我画了2个slot。

执行下一个字节码指令istore_1,所以也会执行字节码分派相关的逻辑。这里需要提醒下,其实之前在介绍字节码指令对应的汇编时,只关注去介绍了字节码指令本身的执行逻辑,其实在为每个字节码指令生成机器指令时,一般都会为这些字节码指令生成3部分机器指令片段:

(1)不同栈顶状态对应的入口执行逻辑;

(2)字节码指令本身需要执行的逻辑;

(3)分派到下一个字节码指令的逻辑。

对于字节码指令模板定义中,如果flags中指令有disp,那么这些指令自己会含有分派的逻辑,如goto、ireturn、tableswitch、lookupswitch、jsr等。由于我们的指令是iconst_0,所以会为这个字节码指令生成分派逻辑,这些生成的逻辑如下:

movzbl 0x1(%r13),%ebx    // %ebx中存储的是字节码的操作码

movabs itos对应的一维数组的首地址,%r10

jmpq *(%r10,%rbx,8)

我们注意到了,如果要让%ebx中存储istore_1的Opcode,则%r13需要加上iconst_0指令的长度,即1。由于iconst_0执行后的出口栈顶缓存为itos,所以要找到入口状态为itos,而Opcode为istore_1的机器指令片段执行。如下图所示。

mov    %eax,-0x8(%r14)

代码将栈顶的值%eax存储到本地变量表下标索引为1的位置处。通过%r14很容易定位到本地变量表的位置,执行完成后的栈状态如下图所示。

执行iconst_0和istore_1时,整个过程没有向表达式栈(上图中sp/rsp开始以下的部分就是表达式栈)中压入0,实际上如果没有栈顶缓存的优化,应该将0压入栈顶,然后弹出栈顶存储到局部变量表,但是有了栈顶缓存后,没有压栈操作,也就有弹栈操作,所以能极大的提高程序的执行效率。

return指令判断的逻辑比较多,主要是因为有些方法可能有synchronized关键字,所以会在方法栈中保存锁相关的信息,而在return返回时,退栈要释放锁。不过我们现在只看针对本实例要运行的部分代码,如下:

// 将JavaThread::do_not_unlock_if_synchronized属性存储到%dl中
0x00007fffe101b770: mov 0x2ad(%r15),%dl
// 重置JavaThread::do_not_unlock_if_synchronized属性值为false
0x00007fffe101b777: movb $0x0,0x2ad(%r15) // 将Method*加载到%rbx中
0x00007fffe101b77f: mov -0x18(%rbp),%rbx
// 将Method::_access_flags加载到%ecx中
0x00007fffe101b783: mov 0x28(%rbx),%ecx
// 检查Method::flags是否包含JVM_ACC_SYNCHRONIZED
0x00007fffe101b786: test $0x20,%ecx
// 如果方法不是同步方法,跳转到----unlocked----
0x00007fffe101b78c: je 0x00007fffe101b970

main()方法为非同步方法,所以跳转到unlocked,在unlocked逻辑中会执行一些释放锁的逻辑,对于我们本实例来说这不重要,我们直接看退栈的操作,如下:

// 将-0x8(%rbp)处保存的old stack pointer(saved rsp)取出来放到%rbx中
0x00007fffe101bac7: mov -0x8(%rbp),%rbx // 移除栈帧
// leave指令相当于:
// mov %rbp, %rsp
// pop %rbp
0x00007fffe101bacb: leaveq
// 将返回地址弹出到%r13中
0x00007fffe101bacc: pop %r13
// 设置%rsp为调用者的栈顶值
0x00007fffe101bace: mov %rbx,%rsp
0x00007fffe101bad1: jmpq *%r13

这个汇编不难,这里不再继续介绍。退栈后的栈状态如下图所示。

这就完全回到了调用Java方法之前的栈状态,接下来如何退出如上栈帧并结束方法调用就是C++语言的事儿了。

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

第6篇-Java方法新栈帧的创建

第7篇-为Java方法创建栈帧

第8篇-dispatch_next()函数分派字节码

第9篇-字节码指令的定义

第10篇-初始化模板表

第11篇-认识Stub与StubQueue

第12篇-认识CodeletMark

第13篇-通过InterpreterCodelet存储机器指令片段

第14篇-生成重要的例程

第15章-解释器及解释器生成器

第16章-虚拟机中的汇编器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

第19篇-加载与存储指令(1)

第20篇-加载与存储指令之ldc与_fast_aldc指令(2)

第21篇-加载与存储指令之iload、_fast_iload等(3)

第22篇-虚拟机字节码之运算指令

第23篇-虚拟机字节码指令之类型转换

第24篇-虚拟机对象操作指令之getstatic

第25篇-虚拟机对象操作指令之getfield

第26篇-虚拟机对象操作指令之putstatic

第27篇-虚拟机字节码指令之操作数栈管理指令

第28篇-虚拟机字节码指令之控制转移指令

第29篇-调用Java主类的main()方法

  

 

  

  

第30篇-main()方法的执行的更多相关文章

  1. Main方法的执行过程(转)

    要运行一个 main 方法 , 首先要知道 main 方法所在的 Class, 在命令行中指定这个 Class 名 Class Lava{ Private int speed = 4; Void fl ...

  2. 1.4 如何在main()方法之前执行输出“hello world”

    public class Test{ static{ System.out.println("hello world"); } public static void main(St ...

  3. jmeter 的java请求代码在main方法里面执行

    1.新建一个java请求执行加法类 public class TestDemo { public int Tdemo(int a,int b){ int sum = 0; sum = a+b; ret ...

  4. jvm——Java main方法的执行

    这是什么神仙博客! https://www.cnblogs.com/kaleidoscope/p/9629156.html

  5. Java中的static关键字解析(转自海子)__为什么main方法必须是static的,因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

    Java中的static关键字解析 static关键字是很多朋友在编写代码和阅读代码时碰到的比较难以理解的一个关键字,也是各大公司的面试官喜欢在面试时问到的知识点之一.下面就先讲述一下static关键 ...

  6. java,maven工程打tar.gz包执行main方法

    一,需要在pom.xml文件添加plugin, 项目目录结构 <build> <plugins> <plugin> <artifactId>maven- ...

  7. Maven项目执行java入口main方法

    在Maven项目中配置pom.xml文件加载maven-surefire-plugin插件来执行testng.xml,相信大家对此种用法已经非常熟悉了.但是有些场景可能需要我们去加载执行java的ma ...

  8. main方法击破

    什么是main方法? 是类中的一段代码,可以让程序独立运行. public class HelloWord{ public static void main(String[] args) { for ...

  9. 10、代码块、构造代码块、静态代码块及main方法之间的关系

    1.普通代码块: 在方法或语句中出现在{}之间的类容就称为普通代码块,简称代码块.普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出现先执行”,即顺序执行. /*下面第一个类时合法的 ...

随机推荐

  1. Go与接口:接口即约定

    接口 接口类型是对其他类型行为的概括与抽象.我们可以通过接口来约定某一类通用行为.Go语言的接口是隐式的:只要实现接口A的所有方法就代表实现了接口A. 接口即约定 接口是什么样的? package i ...

  2. Swift- 设置 UILabel 内边距

    摘要 拿来即用短时间效率虽然挺高的,但是拿来的东西没有消化一次,就无法得心应手的使用它. 这次的探索思路就是,查询官方文档,设置不同的值测试单个方法中参数的变化,之后测试两个方法的执行顺序,处理的思路 ...

  3. configparser读

    #-*-coding:utf-8-*-__author__ = "logan.xu"import configparserconf = configparser.ConfigPar ...

  4. 字符串截取子串(Java substring , indexOf)

    前言 因为之前java课设做的是股票分析系统,我找的接口返回的是一个.csv文件,因为这种文件里面的数据是以逗号分隔的,所以要对数据进行分析的时候需要截取子串,并且以逗号作为截取的标志.所以接下来就说 ...

  5. 一文读懂Base64编码

    Base64编码 字符对应表 上表就是用来表示Base64,一共64个字符,A-Z,a-z,0-9,+,-,还有=(作为补位) 无论将文件,字符串,还是什么转为Base64,一定是用上表的字符表示. ...

  6. MySQL——SQL语句入门

    1.DDL: 数据库定义语言 定义对象:库.表 何为定义: 库的定义: 创建 删除 修改---->修改本身以及库中的对象(表.视图.函数.触发器...) 表的定义: 创建---->定义表的 ...

  7. Python - 面向对象编程 - 三大特性之多态

    前置知识 封装 详解文章:https://www.cnblogs.com/poloyy/p/15203989.html 封装根据职责将属性.方法封装到一个抽象的类中 定义类的准则-封装 继承 详解文章 ...

  8. 尚硅谷 Go语言核心编程资料

    链接:https://pan.baidu.com/s/1zn8Jf82lxg-2msVS1Iedeg  提取码:5vsg  复制这段内容后打开百度网盘手机App,操作更方便哦

  9. 如何在C#中打开和读取EXCEL文件

    这篇文章向您展示如何在C#Windows Forms Application中使用ExcelDataReader,ExcelDataReader.DataSet打开和读取Excel文件.创建一个新的W ...

  10. Reader字符输入流和InputStreamReader和FileReader

    1.FileReader  extends InputStreamReader extends Reader 其中,Reader字符输入流和InputStreamReader和FileReader为字 ...