#局部变量的实现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是这样作的:
图2
首先来描述下上图的几个图形:
还需注意一下几点:
-因为对this,bool,bool2,prefix的分配很是简单,因此这里咱们将这些变量的申明操做并入到一个道1内 -每次为变量分配空间的时候都会从0开始遍历成员变量数组,判断当前声明的变量是否能够和遍历的变量服用,若是能够复用咱们就使用当前遍历的下标分配给当前声明的变量。 对于第二点就是核心问题就是如何判断变量空间是否可复用 .
咱们知道,变量实际存储在局部变量中的,也就是上图中的表格部分,而咱们将存储在这些表格中的局部变量赋予了一个逻辑上的树结构,经过这个结构去判断变量是否可复用,一旦变量能够复用那么他的变量空间也是能够复用的。根据这个树形结构以及上面的图咱们能够得出以步骤来判断变量是否能够复用的(变量的复用是相对与两个变量的),假设咱们如今判断A变量的空间是否能够被B变量复用。
##肯定程序块中可调用的变量
前面介绍了如何判断变量是否能够复用,这里将介绍ASMSupport是如何判断当前所在的做用域能够调用哪些对象的。其实这个逻辑和判断是否能够复用的逻辑正好相反,咱们将做用域看做是一个变量,而后判断是否能够复用,能够复用则说明在该做用域下不能使用指定变量,不然可使用。并且实际上若是是编写代码,咱们可以很直观的看到在子做用域中可以调用父做用域中定义的变量,这里咱们仍是简述下实现逻辑,ASMSupport实现的话则仍是按照图一中的树形结构,假设咱们须要判断A变量是否能够在S做用域中使用。
咱们结合图2中的序号能获得以下判断方法:
##代码实现
###局部变量数组
在图二中咱们看到了局部变量数组的模型,在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
做为父类,必然是须要定义一些基本信息,以下:
这里的componentOrder并不像图二中是一串连续的数字,二是用辈数和点号实现的,相似以下结构:
图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中恒为空。