Java协程框架-Kilim字节码剖析

前面几篇文章从代码层面介绍了Kilim的基本原理,但是对于其中的一些细节,比如Task的执行状态如何管理等问题从代码上依然得不到答案,本文即再深入到字节码层面来解答。

1.  Kilim字节码改写前后的代码有什么区别?

这里还是先上Kilim官方文档中的一张图,这张图清晰的展现出原始的代码与经Kilim改写后的协程代码。

Java协程框架-Kilim字节码剖析_第1张图片

可以看出左边的原始代码,与我们常见的函数相比有所不同,这里显示声明抛出Pausable异常。实际上这个异常在运行期间不会抛出,它的实际作用类似于注解,使得Kilim能够识别哪些代码需要Weaver工具进行字节码改写。

而左边的原始代码与右边改写后的代码相比,原始的不带参数的方法经过字节码改写后多了一个参数Fiber,由于Fiber是依附于Task而存在,因此Fiber在整个Task的执行链中需要全程参与,以期来记录和管理Task的执行状态。

   

那么我们直接反编译字节码看看到底是是不是这样,这里以一个简单的Task为例:

public class PtloginTask extends Task {
    Mailbox mailbox = new Mailbox();

    public PtloginTask() {
       super();
       this.start();
    }

    @Override
    public void execute() throws Pausable, Exception {
       while (true) {
           VerifyRequest req = mailbox.get();
           send(req);
       }
    }
 
    public void send(VerifyRequest req) throws Pausable, Exception {
       req.mbox.put("OK");
    }

    public static class VerifyRequest {
       long uin;
       Mailbox mbox;

       public VerifyRequest(long uin, Mailbox mbox) {
           this.uin = uin;
           this.mbox = mbox;
       }

    }

}

对该Task的源码编译后生成的class文件再使用Kilim提供的Weaver工具处理,最后通过javap命令反编译字节码文件,我们以函数send为例看看生成的字节码。我们发现send函数字节码中确实存在两个版本:

一个是原始的函数版本

public void send(VerifyRequest req) throws Pausable, Exception


另外一个是重载后增加Fiber参数的版本:

public void send(VerifyRequest req, Fiber f) throws Pausable, Exception

Java协程框架-Kilim字节码剖析_第2张图片

同时,我们发现原始的版本的实现已经被Kilim改写成直接调用Task的静态方法errNotWoven,该方法将直接打印出线程堆栈并直接退出,也就是该方法实际上Kilim修改成禁止调用,一旦调用将会抛出类似以下的异常堆栈:

Java协程框架-Kilim字节码剖析_第3张图片

总结一下,函数抛出Pausable异常即表明该函数是可暂停的,Kilim会对这种方法进行Weave处理,处理后会重载一个含有Fiber参数的版本,并禁止直接调用该函数的原始版本。

2.  Kilim改写后的字节码剖析

在分析字节码的执行之前,先简单介绍下JVM运行时线程堆栈的结构.

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。而这每一个方法将都会一个运行时的栈帧结构,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作,栈帧的概念结构如下图所示:

Java协程框架-Kilim字节码剖析_第4张图片

 

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、方法返回地址和一些额外的附加信息。

栈帧中的操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括longdouble32位数据类型所占的栈容量为164位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。而每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

    介绍完上面栈帧的基本结构,下面来解读下javap命令反编译得到的字节码。execute方法和send方法字节码分别如下:

public void execute(kilim.Fiber)   throws kilim.Pausable, java.lang.Exception;

  Code:

   0:  aload_1        //加载局部变量表中索引为1的变量Fiber,并入操作数栈顶

   1:  dup        //复制栈顶的Fiber对象引用,并入栈,此时栈上存在两份Fiber

   2:  astore_2   //将复制的Fiber对象引用出栈,并存储到局部变量表索引为2

   3:  getfield   #44; //Field kilim/Fiber.pc:I

   6:  tableswitch{ //0 to 2

       0: 52;         // pc=0,表示初次进入本函数执行

       1: 35;         // pc=1,表示上次执行到Mailbox.get暂停,直接跳转到该函数

       2: 45;         // pc=2,表示上次执行到send暂停,直接跳转到该函数处执行

       default: 32 }

   32: invokestatic  #47; //Method kilim/Fiber.wrongPC:()V

   35: aload_2        // Mailbox.get恢复执行,这里开始恢复上下文,这一步加载之前

                    存储到局部变量表索引为2处的Fiber对象引用

   36: invokevirtual #51; //Method kilim/Fiber.getCallee:()Ljava/lang/Object;

   39: checkcast  #23; //class kilim/Mailbox//这里确保暂停时为Mailbox

   42: goto   56

   45: aconst_null   // send恢复执行,这里开始恢复上下文

   46: astore_1   //null对象引用出栈,并存储到局部变量表索引为1处,这里因为send函数暂停时,传入的VerifyRequest变量已经在send函数内部保存到对应的State

   47: aload_0        //加载this引用到入操作数栈顶,局部变量表中索引为0总是this

   48: aconst_null   //null对象引用入操作数栈顶

   49: goto   129     // 直接跳转到send函数处执行

   52: aload_0

   53: getfield   #26; //Field mailbox:Lkilim/Mailbox;

   56: aload_2

   57: invokevirtual #55; //Method kilim/Fiber.down:()Lkilim/Fiber;

   60: invokevirtual #59; //Method kilim/Mailbox.get:(Lkilim/Fiber;)Ljava/lang/Object;

   63: aload_2

   64: invokevirtual #63; //Method kilim/Fiber.up:()I

   67: tableswitch{ //0 to 3

       0: 123;        // NOT_PAUSING__NO_STATE  = 0

       1: 123;    // NOT_PAUSING__HAS_STATE = 1

       2: 96;         // PAUSING__NO_STATE       = 2

       3: 121;    // PAUSING__HAS_STATE      = 3

       default: 123 }

   96: pop        // Mailbox.get函数暂停,开始保存栈帧到State

   97: new #65; //class kilim/State

   100: dup

   101: invokespecial #66; //Method kilim/State."":()V

   104: astore_3      // 实例化一个State,并存储到局部变量表的第3个位置

   105: aload_3           // 加载刚刚实例化的State引用入操作数栈

   106: aload_0      

   107: putfield   #70; //Field kilim/State.self:Ljava/lang/Object;

                     //这两步设置State中的self对象,即get函数所属的类实例

   110: aload_3

   111: iconst_1      // 加载常量1入操作数栈

   112: putfield   #71; //Field kilim/State.pc:I    //设置State中的pc=1

   115: aload_2

   116: aload_3

   117: invokevirtual #75; //Method kilim/Fiber.setState:(Lkilim/State;)V

//这一步将实例化的State,对象存储到FiberstateStack

   120: return     //直接返回,标识本函数执行到Mailbox.get暂停

   121: pop

   122: return

   123: checkcast  #7; //class kilim/PtloginTask$VerifyRequest

//这一步从Mailbox.get暂停后恢复执行完成跳转到这里,确保返回VerifyRequest

   126: astore_1   //VerifyRequest变量出栈,并存储到局部变量表索引为1

   127: aload_0    //指向自身的this变量入栈

   128: aload_1       //VerifyRequest变量入栈

   129: aload_2;   //Fiber对象引用入栈

   130: invokevirtual #55; //Method kilim/Fiber.down:()Lkilim/Fiber;

   133: invokevirtual #79; //Method send:(Lkilim/PtloginTask$VerifyRequest;Lkilim/Fiber;)V

   136: aload_2

   137: invokevirtual #63; //Method kilim/Fiber.up:()I

   140: tableswitch{ //0 to 3

       0: 197;    // NOT_PAUSING__NO_STATE  = 0

       1: 197;    // NOT_PAUSING__HAS_STATE = 1

       2: 172;    // PAUSING__NO_STATE       = 2

       3: 196;    // PAUSING__HAS_STATE      = 3

       default: 197 }

   172: new #65; //class kilim/State// send函数暂停,开始保存栈帧到State

   175: dup

   176: invokespecial #66; //Method kilim/State."":()V

   179: astore_3      // 将上面实例化的State出栈,存储到局部变量表的第3个位置

   180: aload_3

   181: aload_0           // 设置State中的self对象,即send函数所属的类实例

   182: putfield   #70; //Field kilim/State.self:Ljava/lang/Object;

   185: aload_3

   186: iconst_2

   187: putfield   #71; //Field kilim/State.pc:I     //设置State中的pc=2

   190: aload_2

   191: aload_3           // 将实例化的State,对象存储到FiberstateStack

   192: invokevirtual #75; //Method kilim/Fiber.setState:(Lkilim/State;)V

   195: return        // send函数暂停,保存栈帧到State成功,最后return

   196: return

   197: goto   52     // while(true)死循环,再次跳转回函数开始处循环执行


public void send(kilim.PtloginTask$VerifyRequest, kilim.Fiber) throws kilim.Pausable, Exception;

  Code:

   0:        aload_2

   1:        getfield    #44; //Field kilim/Fiber.pc:I

   4:        tableswitch{ //0 to 1

                   0: 42;       // pc=0,表示初次进入本函数执行

                   1: 31;       // pc=1,表示上次执行到Mailbox.put暂停,直接跳转到该函数

                   default: 28 }

   28:      invokestatic      #47; //Method kilim/Fiber.wrongPC:()V

   31:      aload_2

   32:      invokevirtual    #51; //Method kilim/Fiber.getCallee:()Ljava/lang/Object;

   35:      checkcast          #23; //class kilim/Mailbox

   38:      aconst_null

   39:      goto 48

   42:      aload_1

   43:      getfield    #87; //Field kilim/PtloginTask$VerifyRequest.mbox:Lkilim/Mailbox;

   46:      ldc    #89; //String OK

   48:      aload_2

   49:      invokevirtual    #55; //Method kilim/Fiber.down:()Lkilim/Fiber;

   52:      invokevirtual    #93; //Method kilim/Mailbox.put:(Ljava/lang/Object;Lkilim/Fiber;)V

   55:      aload_2

   56:      invokevirtual    #63; //Method kilim/Fiber.up:()I

   59:      tableswitch{ //0 to 3

                   0: 113;

                   1: 113;

                   2: 88;

                   3: 112;

                   default: 113 }

   88:      new #65; //class kilim/State

   91:      dup

   92:      invokespecial   #66; //Method kilim/State."":()V

   95:      astore_3

   96:      aload_3

   97:      aload_0

   98:      putfield    #70; //Field kilim/State.self:Ljava/lang/Object;

   101:   aload_3

   102:   iconst_1

   103:   putfield    #71; //Field kilim/State.pc:I

   106:   aload_2

   107:   aload_3

   108:   invokevirtual    #75; //Method kilim/Fiber.setState:(Lkilim/State;)V

   111:   return

   112:   return

   113:   return

上面字节码中提及的setStategetCallee的函数都在Fiber中。

FibersetState函数源码如下,用于Task暂停时保存每一个函数对应的栈帧,这里实际上并非整个函数栈帧,而是保存Task再次恢复执行时所需要的信息。

  /**

     * Called by the generated code before pausing and unwinding its stack

     * frame.

     *

     * @param state

     */

    public void setState(State state) {

        stateStack[iStack] = state;

        isPausing = true;

    }

FibergetCallee函数源码如下,用于Task恢复保存的栈帧时,得到下一个将调用的Pauseable方法的所属对象。

  /**

     * Called by the weaved code while rewinding the stack. If we are about to

     * call a virtual pausable method, we need an object reference on which to

     * call that method. The next state has that information in state.self

     */

    public Object getCallee() {

        assert stateStack[iStack] != PAUSE_STATE : "No callee: this state is the pause state";

        assert stateStack[iStack] != null : "Callee is null";

        return stateStack[iStack + 1].self;

}

Fiber中最重要的几个成员变量:

/**

     * The "program counter", kept equal to stateStack[iStack].pc and is used to jump to the appropriate place in the code while rewinding the code, and also to inform the weaved code inside an exception handler which pausable method (if at all) was being invoked when that exception was thrown.

     *The value 0 refers to normal operation; control transfers to the beginning of the original (pre-weaved) code.

     * A value of n indicates a direct jump into the nth pausable method (after restoring the appropriate state).

     */

    public int                 pc;

    /*

     * One State object for each activation frame in the call hierarchy.

     */

    private State[]            stateStack              = new State[10];
结合上面的字节码清晰的说明 pc 的作用: 如果pc值为0,则表示第一次开始执行,程序执行流程和字节码增强前的流程是一样的;如果pc值为N,则表示直接跳转至本函数中第NPauseable方法处开始执行,说明之前执行到第NPauseable方法时暂停了,此时Task恢复执行,字节码层面通过tableswitch指令将直接跳转该Pauseable方法处执行。

stateStack实际上即用来记录Task的整个调用链上每层Pauseable方法在暂停或恢复运行时的必要的栈帧变量。

3.  Kilim字节码改写后的Task执行链

 将整个Task的调用链串联起来如下图:

Java协程框架-Kilim字节码剖析_第5张图片

你可能感兴趣的:(Java,精华文章,C/C++,精华文章)