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中恆爲空。

相關文章
相關標籤/搜索