Java虛擬機器總結給面試的你(上)

Hugo_Gao發表於2018-02-01

Java虛擬機器一直是Java的重難點,一方面由於系統封裝得太好,你平常寫程式的時候幾乎感覺不到它的存在,另一方面瞭解必要的Java虛擬機器工作原理才能對真實工作環境下的bug進行對症下藥,另外虛擬機器這一部分也一直是面試考官愛問的問題。於是這篇部落格就針對Java虛擬機器的各個知識點進行歸納。

一.Java記憶體區域

執行時資料區域

Java虛擬機器總結給面試的你(上)

程式計數器

程式計數器是當前執行緒執行的位元組碼的行號指示器,執行緒私有,獨立儲存

Java虛擬機器棧

Java虛擬機器棧是執行緒私有,與Java的方法執行模型有關,描述Java方法執行的記憶體模型:方法執行時建立棧幀用於儲存區域性變數表等資訊,方法呼叫返回對應棧幀再虛擬機器中的入棧出棧。

既然是棧那麼深度就是一定的,若執行緒請求棧深度大於虛擬機器所規定的深度,則丟擲StackOverflowError異常。若虛擬機器棧請求擴充套件時無法申請到足夠的記憶體,則丟擲OOM異常。

本地方法棧

就是Native方法所用到的棧,與虛擬機器棧作用類似。

Java堆

Java堆是被所有執行緒共享的一塊記憶體區域,屬於執行緒共享區,在虛擬機器啟動時建立。它主要作用是存放物件例項和進行垃圾收集管理。

方法區

方法區也是各個執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。

執行時常量池

執行時常量池其實屬於方法區,它主要用於存放編譯期生成的各種字面量和符號引用,並且具有動態的特點。

new關鍵字的建立流程

  1. 檢查指令的引數能否在常量池中定位到一個類的符號引用
  2. 檢查是否已經載入解析和初始化
  3. 從Java堆中劃分記憶體給新生物件,使用CAS保證分配的原子性
  4. 將記憶體空間初始化為零值
  5. 對物件進行設定,存放在物件頭中
  6. 執行方法,按照程式設計師的意願進行初始化

分配方式

  1. 指標碰撞

    若Java堆中的記憶體都是規整的,用過的記憶體都在左邊,沒用過的都在右邊,中間指標指向臨界點,分配記憶體就很簡單,只用把指標往右移動和待分配物件一樣的記憶體區域就行了。

  2. 空閒列表

    如果記憶體不是規整的,用過的和沒用過的記憶體交錯在一起,就不能使用指標碰撞了,需要維護一個列表記錄可用的記憶體塊,分配記憶體時就從列表中找一塊足夠大的記憶體記錄下來。

物件的記憶體佈局

物件頭

儲存物件自身的執行時資料,eg:雜湊碼,GC分代年齡,鎖狀態標誌等。還有型別指標指向它的類後設資料的指標,通過這個指標確定這個物件是哪個類的例項。若是Java陣列則物件頭還有一塊記錄陣列長度的資料。

例項資料

程式程式碼中所定義的各種型別的欄位內容,相同寬度的欄位分配到一起

物件訪問定位

虛擬機器通過棧上的reference資料來操作堆上的具體物件。

訪問方式

  1. 使用控制程式碼

    包含物件例項資料與型別資料各自的地址資訊,reference中儲存的就是物件的控制程式碼地址。控制程式碼地址穩定,物件移動時只改變控制程式碼中的例項資料指標,reference本身不修改。

  2. 直接指標

    reference中儲存的就是物件地址,速度更快

二.垃圾收集器與記憶體分配策略

引用計數演算法

給物件新增一個引用計數器,有一個地方引用它時,計數器值就加一,引用失效時就減一,任何時刻計數值為0的物件就死了。這個演算法雖然簡單但是有一個致命的缺點就是無法解決物件之間相互迴圈引用的關係。可達性分析演算法應運而生。

可達性分析演算法

GC Roots作為起點向下搜尋,若一個物件到GC Roots沒有引用鏈的話,則證明此物件不可用,可以回收。搜尋的物件有:

  • 虛擬機器棧中引用的物件
  • 方法區中靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中Native 方法引用的物件

物件的回收經歷

物件在沒有引用鏈通往GC Roots時,需要經過兩次標記才能真正死亡。

  1. 物件在進行可達性分析後如果沒有與GC Roots相連線的引用鏈,會被第一次標記並篩選,若物件沒有覆蓋finalize方法或者已經呼叫過了則不會呼叫finalize。如果需要呼叫finalize方法,則物件被放在F-Queue佇列中,等待執行緒執行。
  2. 物件如果想存活下去,finalize方法是最後的機會,否則GC對F-Queue佇列進行第二次標記後物件真正死亡。

垃圾回收演算法

標記-消除演算法

首先標記出所有需要回收的物件,在標記完成後統一回收,缺點是效率低下而且產生大量的記憶體碎片。

複製演算法

將記憶體劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活的物件複製到另外一塊上面,然後把已經使用的記憶體空間一次清理掉。缺點是將記憶體縮小為了原來的一半,代價較高,物件存活率較高時效率低。

HotSpot實際使用(回收新生代)則是將記憶體劃分為較大的Eden區和兩塊較小的Survivor區,一塊Eden區和一塊Survivor區大小比例為8:1,垃圾回收時就將Eden區和已使用的Survivor區中還存活的物件移到另一塊Survivor區中,由於根據統計,98%的物件都是很快死亡的,所以按照8:1:1的比例來劃分記憶體明顯比1:1劃分記憶體效率要高很多。

標記-整理演算法

標記出需要回收的物件,然後讓所有存活的物件都向一段移動,將另一端的記憶體區域清除掉。

分代收集演算法

根據新生代和老年代的不同特點選擇不同的演算法,新生代使用複製演算法,老年代使用標記清楚或標記整理演算法,虛擬機器實際使用這種演算法。

記憶體分配與回收策略

物件優先在Eden上分配

GC分類

  1. Monior GC,新生代GC,指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特點,所以Monior GC很頻繁,速度也很快
  2. Major GC/Full GC,老年代GC,指發生在老年代的垃圾回收動作,一般比Monior GC慢十倍以上。

大物件直接進入老年代

大物件指需要大量連續記憶體空間的Java物件,如很長的字串以及陣列。直接進入老年代避免頻繁的GC活動。

長期存活的物件將進入老年代

物件在新生代區域每熬過一次Minor GC,年齡就增加一歲(Age Count),超過15歲(預設),就會被晉升到老年代中。

動態年齡判定

如果相同年齡的物件所佔記憶體大於Survivor空間的一半,年齡大於等於該年齡的物件就可以直接進入老年代。

三.類檔案結構

Class類檔案的結構

一組以八位位元組為基礎的二進位制流,各個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有任何分隔符。

儲存結構

無符號數,用來描述數字,索引引用,數量值或UTF-8編碼的字串

,多個無符號數+表=表,_info結尾,Class實際上就是一張表

魔數

每個Class檔案的頭4個位元組,確定這個檔案是否為一個能被虛擬機器接受的Class檔案。class檔案的魔數是0XCAFEBABE。

Class檔案的版本號

緊跟魔數的四個位元組確定版本號:5,6位元組為次版本號,7,8位元組為主版本號。jdk向下相容,不向上相容。

常量池

緊隨主次版本號之後包含:

  • 字面量文字字串,申明為final的常量值。
  • 符號引用
    • 類和介面的全限定名
    • 欄位的名稱和描述符
    • 方法的名稱和描述符
  • 動態連線各個欄位的記憶體資訊,從常量池中獲得對應的讀出引用,再在類建立時或執行解析翻譯到具體的記憶體地址之中。
  • 每一項常量都是一個表,每個表的第一位都是一個是一個u1型別的標誌位,代表這個常量屬於哪種常量型別。

訪問標誌

緊隨常量池後面,兩個位元組代表訪問標誌,標識類或介面的訪問資訊。如這個Class是類還是介面,public型別等。

類索引,父類索引,介面索引集合

除了介面索引是集合外,其他索引都只有一個,用這三個索引確定類的繼承關係。類索引用於確定類的全限定名,父類索引用於確定父類的全限定名。

欄位表集合

用於描述類或介面中宣告的變數,欄位包括類級變數和例項級變數,不包括方法中宣告的區域性變數,描述欄位的屬性如public,static,final等用一個布林變數表示,剛好使用一個標誌位,通過引用常量池中的常量來確定。

方法表集合

與欄位表相似。

屬性表集合

Class檔案,欄位表,方法表都可以攜帶自己的屬性表集合,用於描述某些場景專有的資訊。

位元組碼指令

操作碼長度為一個位元組,所以總數最多不超過256條。。

相關文章