從val跟var瞭解虛擬機器世界

語言: CN / TW / HK

val 跟 var

val本意就是一個不可變的變數,即賦初始值後不可改變,想較於val,var其實就簡單的多,就是可變變數。為什麼說val是不可變的變數呢?這不就是矛盾了嘛,其實不矛盾,我們在位元組碼的角度出發,比如有 val a = Test() var b = Test() 變成的位元組碼是

``` private final Lcom/example/newtestproject/Test; a

private Lcom/example/newtestproject/Test; b ``` 其實val 本質就是用final修飾的變數罷了,而var,就是一個很普通的變數。兩者預設都賦予private作用域,這個其實是kotlin世界賦予的額外操作,並不影響我們的理解。從這裡出發,我們再繼續深入進去!

一個有趣的實驗

companion object{ val c = Test() const val d = "1" const val e = "1" val r = "1" val v = d } 如果我們把val變數放在companion object裡面,這個時候就會被賦予靜態的特性,我們看下上面這段程式碼生成後的位元組碼

```

private final static Lcom/example/newtestproject/Test; c

public final static Ljava/lang/String; d = "1"

public final static Ljava/lang/String; e = "1"

private final static Ljava/lang/String; r

private final static Ljava/lang/String; v ``` 我們可以看到,無論是普通物件還是基本資料型別,都被賦予了static的字首,但是又有稍微不同??我們再來仔細觀察一下。

對於String型別,可以用const關鍵字進行修飾,表示當前的String可用於字串常量進行替換,這個就是完全的替換,直接進行了初始化!而沒有const修飾的字串r,可以看到,只是生成了一個r變數,並沒有直接初始化。而r被初始化的階段,是在clinit階段

static void <clinit>() { ldc "1" putstatic 'com/example/newtestproject/ValClass.r','Ljava/lang/String;' ... 假如說我們用java程式碼去寫的話,比如 public class JavaStaticClass { static final String s = "123"; ... } 所生成的位元組碼是

final static Ljava/lang/String; s = "123" 跟我們kotlin用const修飾的string變數一致,都是直接初始化的!(留到後面解釋)我們繼續深入一點,為什麼有的變數直接就初始化了,有的卻在clinit階段被初始化?那就要從我們的類載入過程說起了!

類載入過程

雖然類載入有很多細分版本,但是這裡筆者引用以下細分版本

image.png 由於類載入過程不是本篇的重點,這裡我們稍微解釋一下各階段的主要任務即可 1. 載入:載入類的過程 :主要是把類的二進位制檔案,轉化為執行時記憶體的資料,包括靜態的儲存結構轉為方法區等操作,在記憶體中生成一個代表這個類的java.lang.Class物件 2. 驗證:驗證class檔案等是否合法:確保Class檔案的位元組流中包含的資訊符合《Java虛 擬機規範》的全部約束要求,保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全。 3. 準備:準備初始資料 :準備階段是正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段 4. 解析:解析常量池,函式符號等 :解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程,這個階段就把我們普通的符號轉化為對記憶體執行資料地址。 5. 初始化:真正的初始化,呼叫clinit:在初始化階段,則會根據程式碼去初始化類變數和其他資源,這個時候,就走到了我們clinit階段了,上面的階段都是由虛擬機器操控,這個階段過去後就正在把控制權給我們程式了

準備階段對static資料的影響

我們主要看到準備階段:準備階段是正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段,即在這個階段過後,所有的static資料被賦予“零值”,以下是零值表

image.png 但是也有例外,就是如果類的屬性表中存在ConstantValue這個特殊的屬性值時,就會在準備階段把真正的常量直接替換給當前的static變數,比如上述程式碼中的

省略companion object const val d = "1" public final static Ljava/lang/String; d = "1" 此時,只要對d的操作,就會被轉化為以下位元組碼,比如 ``` val v = d

位元組碼是 ldc "1" putstatic 'com/example/newtestproject/ValClass.v','Ljava/lang/String;' ``` 變成了ldc指令,即押入了一個字串“1”進了運算元棧上,而原本的d變數盒子,已經徹底被虛擬機器拋棄了。對於屬性表中沒有ConstantValue的變數,就會在初始化階段,即呼叫clinti時,就會把數值賦給相關的變數,以替換“零值”(ps:這裡就是各大位元組碼精簡方案的核心,即刪除把零值賦予零值的相關操作,比如static int xx = 0這種,就可以在Clint階段把相關的賦值位元組碼刪除掉也不影響其原本數值,參考框架bytex)。

當然,我們看到上面的物件c,也是在clinit階段被賦值的,這其實就是ConstantValue生成機制的限制,ConstantValue只會對String跟基本資料型別進行生成,因為我們要替換的常量在常量池裡面!物件肯定是不存在的對不對!

迴歸主題

看到這裡,我們再回來看上面的問題,我們就知道了,kotlin中companion object裡面的字串變數,如果不用const修飾的話,其實對應的字串String型別是不會以ConstantValue生成的,而是以靜態物件相同的方式,在clinit進行!

說了半天!那麼這個又有什麼用呢!?其實這裡主要是為了說明虛擬機器背後生成的原理,同時也是為了提醒!如果以後有做指令優化的需求的時候,就要非常小心kotlin companion object裡面的非const 修飾的String變數,我們就不能在Clinit的時候把這個賦值指令給清除掉!或者說不能跳過Clinit階段就去用這個數值,因為它還是處於未初始化的狀態!

最後

我們從val跟var的角度出發,分析了其背後隱含的故事,當然,看完之後你肯定就徹底懂得了這部分知識啦!無論是以後位元組碼插樁還是面試,相信可以很從容面對啦!

筆者說:如果你看過這篇文章 黑科技!讓Native Crash 與ANR無處發洩!,就會了解到Signal的今生前世,同時我們也釋出了beta版本到maven啦!快來用起來!