在字节码层面,每一个方法都有一个局部变量数组,用来存储当前方法的参数,在方法内声明的变量,如果是非静态方法还要存储当前方法实例的引用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; } }
上面的的代码我们用作用域的方式表现出来如下图:
![](../../resources/images/theory_localvariable_1.png)
如果按照程序流程执行,很显然这里会有两种执行结果。分别是当bool为真的时候执行if语句块,当bool为false执行else语句块。如下图就是这两种情况的局部变量图
![](../../resources/images/theory_localvariable_2.png)
上面前局部变量中,前三个变量是共享的,发生变化的是第后面的变量,对于这两种执行情况,虽然声明的变量类型不同,并且变量字长是不同的,但是由于if和else两个程序块是并行的,所以局部变量中后三个位置是公用的。根据这种情况,ASMSupport采用一种树形结构来模拟和实现作用域和局部变量之间的关系。
我们将上面的代码再修改一下:
_代码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; } }
我们用方形表示程序块,圆形表示局部变量,并且给予各程序块别名得到如下图的树形结构。
![](../../resources/images/theory_localvariable_3.png)
图1
通过这个树,我们能够完成两个事情:
确定哪些变量所占的局部变量空间相对于我们指定的变量是可以复用
确定某一程序块中可以调用哪些变量
在方法内所有的变量都存储在一个局部变量数组中的,但是如果在java代码里每声明一个变量都将它存到局部变量中的一个新的位置,势必会造成很大的空间浪费,正如我们在上面对代码1所分析的,有必要对一些局部变量空间进行些复用。
然我们结合代码2和图1,编译器将代码1转变成class文件,这一个过程中编译器会将程序逐一的转换成字节码,那么扫描的顺序就是对图1中的树做先序遍历(先序遍历其实是针对二叉树的,这里的意义就是先遍历根节点,然后将子节点按从左向右的顺序扫描),得出的结果就是:
this->bool->bool2->prefix-IF->d->s->ELSE-IF2->f->c->l
那么是如何判断变量空间可以复用的呢,ASMSupport是这样做的:
![](../../resources/images/theory_localvariable_4.png)
图2
首先来描述下上图的几个图形:
还需注意一下几点:
-由于对this,bool,bool2,prefix的分配非常简单,所以这里我们将这些变量的申明操作并入到一个道1内 -每次为变量分配空间的时候都会从0开始遍历成员变量数组,判断当前声明的变量是否可以和遍历的变量服用,如果可以复用我们就使用当前遍历的下标分配给当前声明的变量。 对于第二点就是核心问题就是如何判断变量空间是否可复用 .
我们知道,变量实际存储在局部变量中的,也就是上图中的表格部分,而我们将存储在这些表格中的局部变量赋予了一个逻辑上的树结构,通过这个结构去判断变量是否可复用,一旦变量可以复用那么他的变量空间也是可以复用的。根据这个树形结构以及上面的图我们可以得出以步骤来判断变量是否可以复用的(变量的复用是相对与两个变量的),假设我们现在判断A变量的空间是否可以被B变量复用。
前面介绍了如何判断变量是否可以复用,这里将介绍ASMSupport是如何判断当前所在的作用域可以调用哪些对象的。其实这个逻辑和判断是否可以复用的逻辑正好相反,我们将作用域看作是一个变量,然后判断是否可以复用,可以复用则说明在该作用域下不能使用指定变量,否则可以使用。而且实际上如果是编写代码,我们能够很直观的看到在子作用域中能够调用父作用域中定义的变量,这里我们还是简述下实现逻辑,ASMSupport实现的话则还是按照图一中的树形结构,假设我们需要判断A变量是否可以在S作用域中使用。
我们结合图2中的序号能得到如下判断方法:
在图二中我们看到了局部变量数组的模型,在ASMSupport中我们也是采用一个List来作为主体容器。起初我们只是在这个List中每个位置存储最新的变量,比如图二中道4 存储f 的时候,就会将之前的d 覆盖,类似于下图的过程:
![](../../resources/images/theory_localvariable_5.png)
图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
作为父类,必然是需要定义一些基本信息,如下:
这里的componentOrder并不像图二中是一串连续的数字,二是用辈数和点号实现的,类似如下结构:
![](../../resources/images/theory_localvariable_6.png)
图5
那么比较两个Component的先后顺序的话先比较第一个点前面的数字,数字值大的componentOrder比另一个componentOrder大,如果相等则继续比较第二个点前面的数字依次类推,比如“5.1 > 4“, ”6.1.1 > 5.2“, ”6.2 > 6.1.1"。具体实现是在compareComponentOrder方法中实现的。
这个类是对作用域的抽象,也就是我们图二中的方形部分。这个类中主要存储了以下属性:
components和start比较好理解,按照上面解释。但是innerEnd和outerEnd有什么区别呢。这里就要涉及ASMSupport生成作用域的策略,详细参考【ASMSupport作用域划分策略】。
这个类是对局部变量的抽象,在图二中表示为圆形的部分。这个类有下面一些属性:
这里对某些属性做些说明:
A. 模型不同:componentOrder是作用于我们抽象出来的属性结构,如图二中的树形结构中;compileOrder作用于方法生成字节码的模型中,可以认为是编译顺序每执行一次执行队列中的对象,都会把当前执行的序号设置的当前执行的对象的compileOrder 属性中。
B. 作用不同:componentOrder是用来判断变量是否可以复用,变量是否在某一作用域中可用;compileOrder的用来判断当前变量是否可以被某一操作使用,比如System.ou.println(var)中,var的肯定是在调用println方法之前就创建了的,也就意味var的compileOrder肯定要比println操作的compileOrder小。
除了属性这里还介绍下这个类的方法:
这里介绍下store方法
文字描述起来可能比较生涩,具体可以参考代码cn.wensiqun.asmsupport.utils.memory.ScopeLogicVariable.store(),有了上述一些列的操作和模型就能获得变量的一下属性:
再调用MethodVisitor.visitLocalVariable(name, desc, null, start, end, index)的方法,告诉编译器,在start和end范围内,局部变变量位置为index的空间是desc类型的,并且叫做name。这个方法的第三个参数是变量签名,如果使用泛型可以使用,但是ASMSupport暂不支持泛型,所以这个值在ASMSupport中恒为空。