ASMSupport局部变量的实现

#局部变量的实现java

git@osc地址git

在字节码层面,每个方法都有一个局部变量数组,用来存储当前方法的参数,在方法内声明的变量,若是是非静态方法还要存储当前方法实例的引用this。在咱们平时使用java的时候,这个局部变量的大小是在源码编译成class的时候就肯定了的,那么如何更高效的利用这个局部变量,而且合理分配每一个变量对应在局部变量数组中的位置呢,下面咱们就介绍ASMSupport是如何规划局部变量的,先看下面的代码。算法

代码1
public void method(boolean bool) { 
    int prefix = 1;
    if(bool)
    {  
        double d = 2.12;
        String   s = "string";
        ...
    }
    else
    {
        char c = 'a';
        long l  = 1L;
        
    }
}

上面的的代码咱们用做用域的方式表现出来以下图:数组

在此输入图片描述

若是按照程序流程执行,很显然这里会有两种执行结果。分别是当bool为真的时候执行if语句块,当bool为false执行else语句块。以下图就是这两种状况的局部变量图this

在此输入图片描述

上面前局部变量中,前三个变量是共享的,发生变化的是第后面的变量,对于这两种执行状况,虽然声明的变量类型不一样,而且变量字长是不一样的,可是因为if和else两个程序块是并行的,因此局部变量中后三个位置是公用的。根据这种状况,ASMSupport采用一种树形结构来模拟和实现做用域和局部变量之间的关系。spa

咱们将上面的代码再修改一下:.net

_代码2
public void method(boolean bool, boolean bool2)
{
    int prefix = 1;
    if (bool)
    {
        double d = 2.12;
        String s = "string";
    }
    else
    {
        if(bool2)
        {
            float f = 1;
        }
        char c = 'a';
        long l = 1L;
    }
}

咱们用方形表示程序块,圆形表示局部变量,而且给予各程序块别名获得以下图的树形结构。线程

在此输入图片描述 图1调试

经过这个树,咱们可以完成两个事情:code

1. 肯定哪些变量所占的局部变量空间相对于咱们指定的变量是能够复用

2. 肯定某一程序块中能够调用哪些变量

##局部变量空间的复用

在方法内全部的变量都存储在一个局部变量数组中的,可是若是在java代码里每声明一个变量都将它存到局部变量中的一个新的位置,势必会形成很大的空间浪费,正如咱们在上面对代码1所分析的,有必要对一些局部变量空间进行些复用。

然咱们结合代码2和图1,编译器将代码1转变成class文件,这一个过程当中编译器会将程序逐一的转换成字节码,那么扫描的顺序就是对图1中的树作先序遍历(先序遍历实际上是针对二叉树的,这里的意义就是先遍历根节点,而后将子节点按从左向右的顺序扫描),得出的结果就是:

this->bool->bool2->prefix-IF->d->s->ELSE-IF2->f->c->l

那么是如何判断变量空间能够复用的呢,ASMSupport是这样作的:

  • 执行的prefix,将this,bool,bool2,prefix按顺序为其分配局部不变量空间,其下标分别为:0,1,2,3
  • 执行到IF里,任然是为_d ,s_ 分配空间,因为_d_是double,因此分配了空间下标是4和5,_s_分配给了6
  • 执行到IF2了,这时候发现_d_和_s_ 这两个变量的空间我是能够复用的,由于IF和ELSE是并行的,同一时刻同一线程不可能同时执行到IF和ELSE,而IF2又是ELSE的子块,因此它将f 分配给了下标为4的空间,这里4位置上已经被变量d 和s 复用了。
  • 继续执行到_c_和_l_的时候,发现刚才分配给f 变量的空间是能够复用的,由于f 所在的程序块是IF2,他是ELSE的一个自程序块,在这个程序块的做用域中声明的变量只在当前做用域下有效,因此将_c_分配给下标为 f 所分配的空间4,这时候4位置已经被_d f c_ 三个变量共享了;这时候继续变量到l , 因为l是long型占两个字的空间,一样发现d所占位置5和s所占位置6是能够共享的,因此将5和6位置的局部变量分配给l

在此输入图片描述 图2

首先来描述下上图的几个图形:

  • **方形:**表示程序块,也能够叫作_做用域_
  • **圆形:**表示变量,其中数字表示ASMSupport遍历对象的顺序,咱们称之为_变量序号_,后面的表示_变量名 _
  • **直线:**表示程序块-程序块,程序块-变量之间的从属关系
  • **横向的矩形:**表示在这个树结构中的_辈份_ ,而矩形右边的数字表示辈数,好比第一辈,第二辈
  • **带箭头的虚线:**ASMSupport对变量建立遍历的路径
  • **竖虚线和虚线间的椭圆形:**用来划分每次变量声明以及为该变量的局部变量数组分配,咱们姑且称之为道,配椭圆形内的数字,咱们称之为_道1,道2_
  • **表格:**局部变量数组

还需注意一下几点:

-因为对this,bool,bool2,prefix的分配很是简单,因此这里咱们将这些变量的申明操做并入到一个道1内 -每次为变量分配空间的时候都会从0开始遍历成员变量数组,判断当前声明的变量是否能够和遍历的变量服用,若是能够复用咱们就使用当前遍历的下标分配给当前声明的变量。 对于第二点就是核心问题就是如何判断变量空间是否可复用 .

咱们知道,变量实际存储在局部变量中的,也就是上图中的表格部分,而咱们将存储在这些表格中的局部变量赋予了一个逻辑上的树结构,经过这个结构去判断变量是否可复用,一旦变量能够复用那么他的变量空间也是能够复用的。根据这个树形结构以及上面的图咱们能够得出以步骤来判断变量是否能够复用的(变量的复用是相对与两个变量的),假设咱们如今判断A变量的空间是否能够被B变量复用。

  • 判读A变量的遍历序号是否小于B变量的遍历序号,若是大于则不能复用,不然进入2
  • 若是A的辈份和B的辈份相同(在图二中表示为辈数值相同,好比变量d,s,c,l)而且具备同一个父辈,说明不能复用,不一样说明容许复用。
  • 若是A变量的辈数大于B变量的辈数(好比图二中的f和c),则A变量的空间能够被复用。
  • 若是A变量的辈数小于B变量的辈数(好比图二中的d和f),从B变量开始向上获取长辈(做用域),直到找到的长辈和A变量的辈数相同的做用域T,若是A和T是同一个父辈则不能复用,若是父辈不一样则能够复用。

##肯定程序块中可调用的变量

前面介绍了如何判断变量是否能够复用,这里将介绍ASMSupport是如何判断当前所在的做用域能够调用哪些对象的。其实这个逻辑和判断是否能够复用的逻辑正好相反,咱们将做用域看做是一个变量,而后判断是否能够复用,能够复用则说明在该做用域下不能使用指定变量,不然可使用。并且实际上若是是编写代码,咱们可以很直观的看到在子做用域中可以调用父做用域中定义的变量,这里咱们仍是简述下实现逻辑,ASMSupport实现的话则仍是按照图一中的树形结构,假设咱们须要判断A变量是否能够在S做用域中使用。

咱们结合图2中的序号能获得以下判断方法:

  • A的遍历序号大于S的遍历序号
  • 若是A和S的辈份相同,而且具备同一父辈,说明A能够在S做用域内使用
  • 若是A的辈数大于S的辈数则A不能在S做用域内使用
  • 若是A的辈数小于S的辈数,则从S辈数向上获取长辈,直到找到的长辈和A变量的辈数相同的做用域T,若是A和T是同一个父辈则能够在S中使用A,不然不能使用。

##代码实现

###局部变量数组

在图二中咱们看到了局部变量数组的模型,在ASMSupport中咱们也是采用一个List来做为主体容器。起初咱们只是在这个List中每一个位置存储最新的变量,好比图二中道4 存储f 的时候,就会将以前的d 覆盖,相似于下图的过程:

在此输入图片描述 图3

可是因为咱们但愿经过【如何查看ASMSupport的log文件】,在生产每一条局部变量操做指令的时候都打印出当前局部变量状态,这样更便于咱们调试和跟踪本身的程序。因此咱们在局部变量这个List的容器中存储的是一个自定义的类LocalHistory的对象,每个LocalHistory对象对应一个本地变量数组中的一个单元位置,好比图二中的局部变量d 是double类型的,占两个单元,因此将会建立两个LocalHistory对象,而且在LocalHistory类中经过一个List存储在该位置上局部变量的变动历史,也就是咱们图二中的局部变量的结构。

这些逻辑在ASMSupport代码中使用cn.wensiqun.asmsupport.utils.memory.LocalVariables 和 cn.wensiqun.asmsupport.utils.memory.LocalVariables.LocalHistory 实现的。后者是前者的一个内部类,而且是一个静态私有类型,仅仅在内部被LocalVariables使用。

LocalVariables还有个功能是打印局部变量的状态,这部分代码并非局部变量实现的核心因此不作解释。

###做用域和局部变量的逻辑抽象

在图2中的核心是做用域和局部变量的树结构,做为树中的每个节点,咱们为其定义一个父类cn.wensiqun.asmsupport.core.utils.memory.Component,再分别定义Component的两个子类cn.wensiqun.asmsupport.core.utils.memory.Scope和cn.wensiqun.asmsupport.core.utils.memory.ScopeLogicVariable表示做用域和局部变量。层级结构图以下:

Component
    |-Scope
    |-ScopeLogicVariable
_图4

Component

做为父类,必然是须要定义一些基本信息,以下:

  • locals: 这个是一个LocalVariabbles对象的引用
  • generation: 存储该节点在树形结构中的辈数,对应予图二中的横向矩形
  • componentOrder : 表示出现的顺序,对应于图二中每个节点前的数字
  • parent : 表示直接的父辈

这里的componentOrder并不像图二中是一串连续的数字,二是用辈数和点号实现的,相似以下结构:

在此输入图片描述 图5

那么比较两个Component的前后顺序的话先比较第一个点前面的数字,数字值大的componentOrder比另外一个componentOrder大,若是相等则继续比较第二个点前面的数字依次类推,好比“5.1 > 4", "6.1.1 > 5.2", "6.2 > 6.1.1"。具体实现是在compareComponentOrder方法中实现的。

Scope

这个类是对做用域的抽象,也就是咱们图二中的方形部分。这个类中主要存储了如下属性:

  • components : 一个List类型,存储这个的子节点
  • start:【参考字节码Label】,用来划定当前做用域的起始位置
  • innerEnd :【参考字节码Label】,用来划定当前做用域的结束位置
  • outerEnd:【参考字节码Label】,用来划定当前做用域的结束位置

components和start比较好理解,按照上面解释。可是innerEnd和outerEnd有什么区别呢。这里就要涉及ASMSupport生成做用域的策略,详细参考【ASMSupport做用域划分策略】。

ScopeLogicVariable

这个类是对局部变量的抽象,在图二中表示为圆形的部分。这个类有下面一些属性:

  • String name : 变量名
  • Type declareType : 变量的声明类型
  • Type actuallyType : 变量的实际类型
  • int[] positions : 变量所占局部变量数组的位置
  • **int initStartPos :**变量在局部变量的中的起始位置
  • **boolean anonymous :**是不是匿名变量
  • **Label specifiedStartLabel :**变量所在做用域的起始位置
  • int compileOrder : 生成变量指令在字节码中的编译顺序

这里对某些属性作些说明:

  • **1)actuallyType:**这个属性表示变量的实际类型,可是这个属性不彻底可以肯定变量的实际类型,好比我经过调用方法获取到的一个对象,我仅仅只能将方法的返回类型做为actuallType,可是方法返回的类型极可能是个接口,因此这个属性不建议使用。
  • 2)positions : 这个属性是个数组,缘由是若是当前变量是个double或者long类型,是占两个单位的局部变量空间的,因此这里用数组来存储,能够确定的若是这个数组里面的有值,必定是连续的,好比[1,2], [3,4],这是由于局部变量空间的存储就是一个连续的存储。固然这个数组也可能没有值,由于在上面咱们介绍过,变量空间是可能被复用的,一旦他某个位置被复用率,这里的数组就为变,好比图二中第三道 d变量的positions应该是[4,5], 到了第四道 就变成了5, 而新建立的变量 c 的positions就变成了4.
  • 3) initStartPos : 这个表示该变量在局部变量数组中所占空间的起始位置,这个值等于positions数组在最初状态的第0个下标的值,为何说是最初状态,前面在介绍positions的时候有介绍,positions是一直在变化的,因此咱们在第一次初始化positions的时候就将其第0个下标的值赋予到initStartPos属性。
  • 4) anonymous : 这个属性表示变量是否为匿名,一旦这个属性是true,那么name属性则失效
  • 5) compileOrder : 根据上面的解释,这个属性和其父类的属性componentOrder有类似之处。其区别有两个地方

A. 模型不一样:componentOrder是做用于咱们抽象出来的属性结构,如图二中的树形结构中;compileOrder做用于方法生成字节码的模型中,能够认为是编译顺序每执行一次执行队列中的对象,都会把当前执行的序号设置的当前执行的对象的compileOrder 属性中。

B. 做用不一样:componentOrder是用来判断变量是否能够复用,变量是否在某一做用域中可用;compileOrder的用来判断当前变量是否能够被某一操做使用,好比System.ou.println(var)中,var的确定是在调用println方法以前就建立了的,也就意味var的compileOrder确定要比println操做的compileOrder小。

除了属性这里还介绍下这个类的方法:

  • isShareable : 这个方法传入一个ScopeLogicVariable类型的参数var,判断当前变量空间是否能够被传入的参数复用,具体算法见上文【局部变量空间的复用】
  • availableFor : 传入一个Component,判断在Component中是否可使用当前变量,算法见上文【肯定程序块中可调用的变量】
  • **isSubOf :**判断当前变量是不是传入的Scope的子代。
  • store : 将当前变量存入局部变量数组。

这里介绍下store方法

  • 设当前变量为C
  • 获取C所须要的局部变量单位空间个数N
  • 从0 下标开始遍历局部变量数组,设I为遍历的次数(从0开始),若是有变量还没遍历,设V(咱们称之为幸存者survivor)为下一个须要遍历的对象进入4,不然进入7。
  • 若是V的所占的空间能够被C所复用,进入5,不然进入6
  • 删除Vpositions的第一个位置,而且将I加入到C的positions 中,同时将C存入到局部变量的I位置,令N=N-1,若是N等于0则跳出循环,不然进入3
  • 若是CV都是非匿名变量,判断C的名字和V是否相同,若是相同抛出异常,不然进入3
  • 到这一步说明全部可复用变量空间都已经判断完成,若是N依然大于0,则存NC到局部变量的末尾处,而且将每次存入到局部变量数组的位置添加到Cpositions中。进入8
  • CinitStartPos等于Cpositions下标为0的值。

文字描述起来可能比较生涩,具体能够参考代码cn.wensiqun.asmsupport.utils.memory.ScopeLogicVariable.store(),有了上述一些列的操做和模型就能得到变量的一下属性:

  • **name:**变量名
  • **desc:**变量声明类型
  • **start:**变量所在做用域的起始位置,对应于所在Scope的start
  • **end :**变量所在做用域的结束位置,对应于所在Scope的innerEnd
  • index : 变量在局部变量数组的其实下标值,对应于initStartPos

再调用MethodVisitor.visitLocalVariable(name, desc, null, start, end, index)的方法,告诉编译器,在start和end范围内,局部变变量位置为index的空间是desc类型的,而且叫作name。这个方法的第三个参数是变量签名,若是使用泛型可使用,可是ASMSupport暂不支持泛型,因此这个值在ASMSupport中恒为空。

相关文章
相关标签/搜索