Java 強/弱/軟引用,Java字節碼指令

關於Java的強/軟/弱引用,今天總結一下他們的區別和應用。

引用的強弱程度

根據JVM對三種引用的內存回收時機來區分的話,可以把他們按

強引用 > 軟引用 > 弱引用

來排列。在JVM運行內存不足時,這三種之中最先被回收的是 弱引用,依次到最後纔是強引用(不會被回收)。
但是對於強引用來說,JVM在內存不足時寧可拋出 OOM,也不會隨意回收強引用來釋放內存。
下面具體說下強引用。

強引用 Strong Reference

在最經常實例化對象的語法裏,如果不指定引用類型,那麼默認是強引用。

Object  Object = new Object();

分兩種情況來說明一下。
· 在方法中的強引用
· 全局強引用

方法中的強引用

在方法內聲明一個強引用對象的話,在內存中會分兩部分來進行。首先引用會保存在Stack中,而引用的對象Object會存放在堆中。

圖片

上面的圖說明了JVM的內存模型和各種對象存放的位置。
當方法執行完後,會退出方法棧,此時引用不在,所以Object會被回收。

全局強引用

其實在JVM中沒有全局變量這種概念,相對的是全局靜態變量。我們可以看一個類Global在編譯後的字節碼,

public class Global {
   Global global = new Global();

   public Global(){

   }

   public static void main(String[] args) {
     System.out.println("global");
   }
}

javap -c Global.Class

public class Global {
 Global global;

 public Global();
   Code:
      0: aload_0
      1: invokespecial #1                  // Method java/lang/Object."<init>":()V
      4: aload_0
      5: new           #2                  // class Global
      8: dup
      9: invokespecial #3                  // Method "<init>":()V
     12: putfield      #4                  // Field global:LGlobal;
     15: return

 public static void main(java.lang.String[]);
   Code:
      0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      3: ldc           #6                  // String global
      5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      8: return
}

可以看出來,其實非靜態的變量也是在默認構造方法中實例化的,但是靜態變量就不同了。靜態變量是在堆中存放引用和對象,
所以全局靜態引用需要在不使用時將它置爲null

object = null;

軟引用 SoftReference

軟引用在JVM內存不足時會被回收,用這種特性,可以在一些內存敏感的場景上用軟引用。
比如Bitmap對象,可以用軟引用

SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>();

弱引用 WeakReference

弱引用有着比軟引用更脆弱的生命週期。即使內存充足,但是隻要被GC掃描到就會被回收

WeakReference<String> abcWeakRef = new WeakReference<String>(str);

前言

隨着Java開發技術不斷被推到新的高度,對於Java程序員來講越來越需要具備對更深入的基礎性技術的理解,比如Java字節碼指令。不然,可能很難深入理解一些時下的新框架、新技術,盲目一味追新也會越來越感乏力。

本文既不求照本宣科,亦不求炫技或著文立說,僅力圖以最簡明、最形象生動的方式,結合例子與實戰,讓小白也能搞懂這門看似複雜的技術概念。

單刀直入

閒言碎語不要講,先表一表,什麼是Java字節碼指令?簡而言之,Java字節碼指令就是Java虛擬機能夠聽得懂、可執行的指令,可以說是Jvm層面的彙編語言,或者說是Java代碼的最小執行單元。
有點Java基礎的人一定都知道,javac命令會將Java源文件編譯成字節碼文件,即.class文件,其中就包含了大量的字節碼指令。因此可以將javac命令理解爲一個翻譯命令,將源文件翻譯成Jvm可以執行的指令。
那麼最直觀的探究方法莫過於直接對比翻譯前後的內容。
具體如何對比呢?就不得不用到Java爲我們一直默默提供的一項利器,javap命令,它可以解析字節碼,將字節碼內部邏輯以可讀的方式呈現出來。爲了緊貼實戰,我們直接在新建的Java工程裏,寫這樣一個UserServiceImpl類,裏面包含幾個由簡單到複雜的方法,以及一個名爲serviceType的屬性:

clipboard.png

如圖,以上方法,複雜度由低到高依次爲:getServiceType<setServiceType<genToken<login(以及一個實例代碼塊),後面我也會按照這個順序解讀其字節碼指令的執行邏輯。
下面我們編譯工程,然後在下圖所示的目錄(gradle編譯工程)找到該類的字節碼文件:

圖片描述

cd到這個路徑下,運行javap命令:

javap -v -p UserServiceImpl

就可以觀看到翻譯版的Java字節碼的胴體了!這裏的-v意思是囉嗦模式,會輸出全面的字節碼信息,而-p是指涵蓋所有成員。原字節碼信息輸出內容較多,基於本文的目標,取其一方法的內容,整理如下圖:
方法1,getServiceType():

圖片描述

這個getServiceType的方法應該是再簡單不過的Java代碼,翻譯成字節碼後也變成了三行,我們先來簡單推理一下:第一句,aload_0不知所云,索性略過;第二行,getfield應該可以讀懂,後面這個#8似乎是他的參數(實際上是對常量池的引用),//後面註釋的內容是javap給我們加上的,意思應該是#8的指向是"Field serviceType:Ljava/lang/String;"這個內容。
所以getfield這一行就是取出serviceType這個字段嘍,so easy。areturn肯定就是return的意思,a的含義也先略過不表。總之就是取出serviceType字段然後return嘍。

那麼現在的問題就是aload_0是什麼意思了,看似多餘,但仔細思考一下,似乎之前給getfield指令傳入了“Field serviceType:Ljava/lang/String;”這樣一個並不完整的參數,其後半部分的“Ljava/lang/String;”僅僅表示這個serviceType字段的類型是String,也就是說,整個參數裏沒有說是取的誰的serviceType字段啊!究竟是get誰的feild呢?

由此可以想到:aload操作一定是在爲getfield指令準備了一個主體。

實際上,再結合下面的局部變量表,aload_0中的0正是局部變量表裏的Slot 0的含義。意思是將局部變量表裏的Slot 0的東西壓入操作數棧,這個Slot 0裏的東西name正是this,也就是UserServiceImpl的實例,即getfield的主體。

clipboard.png

大戲上演

好了,對於小白同學有些陌生的概念來了,啥是操作數棧?啥是局部變量表?
其實這兩個東西理解好了,關於虛擬機指令就懂了一大半了。
那麼,不妨刪繁就簡,由易入難,先講一個這樣的故事,故事起名叫:

Java方法之創世紀

話說Jvm大帝是神之旨意的履行者(Jvm大帝就是虛擬機,神就是開發者,神之旨意是開發者寫好並編譯後的字節碼...),當Jvm大帝帶領Java世界運行進入了一個新的方法後,會爲這個方法在棧內存大陸上創造兩個重要的領域:局部變量表和操作數棧。

要有棧。要有表。神說。

依照神之旨意,jvm大帝創造的局部變量表裏一般會包含this指針(針對實例方法,靜態方法當然無此)、方法的所有傳入參數和方法中所開闢的本地變量。

那麼操作數棧是幹嘛用的呢?

我們再引入另外一個比喻,如果把運行Java方法理解爲拍戲,那麼局部變量表裏的各個局部變量就是這部戲的核心主角,或者說領銜主演,而操作數棧正是這部戲的舞臺。所謂操作數棧搭臺,局部變量唱戲,是也。那麼aload_0就是告訴Jvm導演(大帝已淪落爲導演),請0號演員this同志登臺(壓棧),演後邊的本子。
當然了,這個比喻並不完全恰當,因爲操作數棧並不是“舞臺”的結構,而是棧的結構。但是這個比喻可以很好地說明局部變量表和操作數棧之間的關係,以及aload_0的作用。

下面我們用一張圖來演示一下getServiceType這個小劇本橋段所導演的故事:

圖片描述

好吧這部劇雖然短的可憐,但已經基本把指令、操作數棧和局部變量表三者的關係演繹了出來。
值得注意的是,getfield這條指令對操作數棧進行了複合操作,其流程可以示意如下圖:

彈出-&gt;取值-&gt;壓回

後面我們將要接觸到的許多指令都如此,指令內部執行了彈出—>處理—>壓回的流程。
下面我們就來分析一個相對複雜一點的方法,setServiceType(String),如下圖:

圖片描述

這裏我們看到,變化主要有,指令多了一行,多進行了一次aload,getfield變成了putfield,areturn變成了return,僅此而已。另外領銜主演也就是局部變量表裏多了一位,也就是方法的傳入參數serviceType字符串對象了。其情節如下:

圖片描述

這裏,putfield只彈出棧內的操作數,而沒有向操作數棧壓回任何數據,而且執行putfield之前,棧內元素的位置也必須符合“值在上,主體在下”要求。
而最後的return僅表示方法結束,而不會像areturn一樣返回棧頂元素。這也印證了setServiceType(String)方法沒有返回參數。

融會貫通

相信有了以上的講解,大家對指令、操作數棧、局部變量表三者的運作關係有了一定認識,爲了後邊能夠分析更復雜的方法,這裏必須概括性地講解一下更多的Java字節碼指令。雖然Java字節碼指令非常多,但其實常用的不外乎幾個類別,先從這幾個常用類別入手理解,便可漸入佳境。
關於字節碼指令的分類,可以從兩個維度進行:一是指令的功能,二是指令操作的數據類型。我們先從功能說起,指令主要可以分爲如下幾類:

  1. 存儲和加載類指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用於在局部變量表、操作數棧和常量池三者之間進行數據調度;(關於常量池前面沒有特別講解,這個也很簡單,顧名思義,就是這個池子裏放着各種常量,好比片場的道具庫)
  2. 對象操作指令(創建與讀寫訪問):比如我們剛剛的putfield和getfield就屬於讀寫訪問的指令,此外還有putstatic/getstatic,還有new系列指令,以及instanceof等指令。
  3. 操作數棧管理指令:如pop和dup,他們只對操作數棧進行操作。
  4. 類型轉換指令和運算指令:如add/div/l2i等系列指令,實際上這類指令一般也只對操作數棧進行操作。
  5. 控制跳轉指令:這類裏包含常用的if系列指令以及goto類指令。
  6. 方法調用和返回指令:主要包括invoke系列指令和return系列指令。這類指令也意味這一個方法空間的開闢和結束,即invoke會喚醒一個新的java方法小宇宙(新的棧和局部變量表),而return則意味着這個宇宙的結束回收。

如下圖,展示了各類指令的作用:

圖片描述

再從另外一個維度,即指令操作的數據類型來講:指令開頭或尾部的一些字母,就往往表明了它所能操作的數據類型:

a對應對象,表示指令操作對象性數據,比如aload和astore、areturn等等。
i對應整形。也就有iload,istore等i系列指令。
f對應浮點型。
l對應long,b對應byte,d對應double,c對應char。
另外地,ia對應int array,aa對應object array,da對應double array。不在一一贅述。

瞭解了以上內容,我們再去看最後幾個方法,應該就會容易理解很多了。
下面我們就直搗黃龍genToken這個方法(圖中的顏色暗示了指令和方法調用之間的關係):

clipboard.png

這個過程簡單解讀如下:
1.new一個StringBuilder對象(在堆內存中開闢空間),並將其引用入棧,用於實現加號連接字符串功能(相當於C++中的運算符重載);
2.dup複製棧頂的剛剛放入的引用,再次壓棧,這時棧裏有兩個重複的內容,深度爲2;
3.調用並彈出棧頂StringBuilder引用對象的<init>方法,棧深度爲1;
4.(綠色部分)調用UUID.randomUUID()靜態方法,結果壓棧後彈出調用String的toString方法,再壓棧,棧深度爲2;
5.(黃色部分)將"-"和""字符壓棧,此時棧深度爲4,彈出(棧頂3個元素)調用replace方法,結果壓棧,深度爲2;
6.調用StringBuilder對象的append方法,結果壓棧,深度爲1;
7.(藍色部分)將參數user壓棧並調用hashCode方法,結果壓棧,深度爲2;
8.調用StringBuilder對象的append方法(此處和上面的append調用共同完成了加號功能,在圖中爲紅色部分),結果壓棧,深度爲1,再調用toString方法後結果壓棧,深度爲1;
9.areturn返回棧頂對象。

再看這個包含if跳轉的方法login:

clipboard.png

如上圖,圖中已經說明的比較全面了,不再贅述。值得一提的是,Java的這種基於棧結構的指令,在設計上有一種非常簡潔的美感,指令與指令之間並沒有較重的依賴,每條指令僅僅與操作數棧等領域內的數據發生關係,充滿着某種平衡與秩序感。因此也必須注意,幾乎每條指令的運行都有其前提,比如在invokevirtual或invokespecial指令執行前,必須保證操作數棧內提前按順序壓入好所需的操作數,否則就會發生問題。
關於最複雜的onCreate方法,就不再囉嗦解讀了,讀者可以前往我的github上的對應demo repo,進入tutorial分支,拉取源碼和教程資源,或者自己寫demo體驗這一完整過程。
地址:https://github.com/BryanSharp...

後話

關於實戰,一是可以學習使用強大開源工具ASM.jar;二是,可以參考本人的另一篇文章:Java字節碼修改神器HiBeaver:黑掉你的SDK以及一次Android字節碼插樁實戰,利用hibeaver這個助手,開發者可以非常靈活地對字節碼進行修改,插入指令,hook代碼,甚至建立一些簡單的AOP框架,對於Java字節碼學習大有裨益。
hibeaver完全開源,github項目地址:https://github.com/BryanSharp...

祝玩的愉快!
本文如有不妥之處,歡迎交流指正。

另外,本文爲了儘可能地簡明生動、直入核心,簡化了很多概念和細節,讀者須知實際情況的更爲複雜。但相信在理解了本文以後,就可以抓住Java字節碼指令的核心理念,也就算扣開虛擬機學習的大門並可以開始讀書精進了。下面盜圖一張(後有出處),可作拓展:

圖片描述

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章