zoukankan      html  css  js  c++  java
  • 第30篇-main()方法的执行

    在第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()方法

      

     

      

      

  • 相关阅读:
    linux常用命令
    linux简介
    vmware+CentOs 6.9的安装步骤
    Android layer type与WebView白屏-第一次加载webview很慢
    Android View框架的measure机制
    maven的facet错误解决方法
    oracle jdbc驱动发布nexus
    dubbox2.8.4编译发布到私服(nexus)
    一致性哈希 与 普通哈希对比
    1对多业务,数据库水平切分架构一次搞定 | 架构师之路
  • 原文地址:https://www.cnblogs.com/mazhimazhi/p/15405536.html
Copyright © 2011-2022 走看看