#局部變量的實現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中恆爲空。