Java虛擬機一直是Java的重難點,一方面由于系統封裝得太好,你平常寫程序的時候幾乎感覺不到它的存在,另一方面了解必要的Java虛擬機工作原理才能對真實工作環境下的bug進行對癥下藥,另外虛擬機這一部分也一直是面試考官愛問的問題。于是這篇博客就針對Java虛擬機的各個知識點進行歸納。
一.Java內存區域
運行時數據區域
程序計數器
程序計數器是當前線程執行的字節碼的行號指示器,線程私有,獨立存儲
Java虛擬機棧
Java虛擬機棧是線程私有,與Java的方法執行模型有關,描述Java方法執行的內存模型:方法執行時創建棧幀用于儲存局部變量表等信息,方法調用返回對應棧幀再虛擬機中的入棧出棧。
既然是棧那么深度就是一定的,若線程請求棧深度大于虛擬機所規定的深度,則拋出StackOverflowError異常。若虛擬機棧請求擴展時無法申請到足夠的內存,則拋出OOM異常。
本地方法棧
就是Native方法所用到的棧,與虛擬機棧作用類似。
Java堆
Java堆是被所有線程共享的一塊內存區域,屬于線程共享區,在虛擬機啟動時創建。它主要作用是存放對象實例和進行垃圾收集管理。
方法區
方法區也是各個線程共享的內存區域,用于儲存已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據。
運行時常量池
運行時常量池其實屬于方法區,它主要用于存放編譯期生成的各種字面量和符號引用,并且具有動態的特點。
new關鍵字的創建流程
- 檢查指令的參數能否在常量池中定位到一個類的符號引用
- 檢查是否已經加載解析和初始化
- 從Java堆中劃分內存給新生對象,使用CAS保證分配的原子性
- 將內存空間初始化為零值
- 對對象進行設置,存放在對象頭中
- 執行<init>方法,按照程序員的意愿進行初始化
分配方式
-
指針碰撞
若Java堆中的內存都是規整的,用過的內存都在左邊,沒用過的都在右邊,中間指針指向臨界點,分配內存就很簡單,只用把指針往右移動和待分配對象一樣的內存區域就行了。
-
空閑列表
如果內存不是規整的,用過的和沒用過的內存交錯在一起,就不能使用指針碰撞了,需要維護一個列表記錄可用的內存塊,分配內存時就從列表中找一塊足夠大的內存記錄下來。
對象的內存布局
對象頭
儲存對象自身的運行時數據,eg:哈希碼,GC分代年齡,鎖狀態標志等。還有類型指針指向它的類元數據的指針,通過這個指針確定這個對象是哪個類的實例。若是Java數組則對象頭還有一塊記錄數組長度的數據。
實例數據
程序代碼中所定義的各種類型的字段內容,相同寬度的字段分配到一起
對象訪問定位
虛擬機通過棧上的reference數據來操作堆上的具體對象。
訪問方式
-
使用句柄
包含對象實例數據與類型數據各自的地址信息,reference中儲存的就是對象的句柄地址。句柄地址穩定,對象移動時只改變句柄中的實例數據指針,reference本身不修改。
-
直接指針
reference中儲存的就是對象地址,速度更快
二.垃圾收集器與內存分配策略
引用計數算法
給對象添加一個引用計數器,有一個地方引用它時,計數器值就加一,引用失效時就減一,任何時刻計數值為0的對象就死了。這個算法雖然簡單但是有一個致命的缺點就是無法解決對象之間相互循環引用的關系。可達性分析算法應運而生。
可達性分析算法
GC Roots作為起點向下搜索,若一個對象到GC Roots沒有引用鏈的話,則證明此對象不可用,可以回收。搜索的對象有:
- 虛擬機棧中引用的對象
- 方法區中靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中Native 方法引用的對象
對象的回收經歷
對象在沒有引用鏈通往GC Roots時,需要經過兩次標記才能真正死亡。
- 對象在進行可達性分析后如果沒有與GC Roots相連接的引用鏈,會被第一次標記并篩選,若對象沒有覆蓋finalize方法或者已經調用過了則不會調用finalize。如果需要調用finalize方法,則對象被放在F-Queue隊列中,等待線程執行。
- 對象如果想存活下去,finalize方法是最后的機會,否則GC對F-Queue隊列進行第二次標記后對象真正死亡。
垃圾回收算法
標記-消除算法
首先標記出所有需要回收的對象,在標記完成后統一回收,缺點是效率低下而且產生大量的內存碎片。
復制算法
將內存劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將還存活的對象復制到另外一塊上面,然后把已經使用的內存空間一次清理掉。缺點是將內存縮小為了原來的一半,代價較高,對象存活率較高時效率低。
HotSpot實際使用(回收新生代)則是將內存劃分為較大的Eden區和兩塊較小的Survivor區,一塊Eden區和一塊Survivor區大小比例為8:1,垃圾回收時就將Eden區和已使用的Survivor區中還存活的對象移到另一塊Survivor區中,由于根據統計,98%的對象都是很快死亡的,所以按照8:1:1的比例來劃分內存明顯比1:1劃分內存效率要高很多。
標記-整理算法
標記出需要回收的對象,然后讓所有存活的對象都向一段移動,將另一端的內存區域清除掉。
分代收集算法
根據新生代和老年代的不同特點選擇不同的算法,新生代使用復制算法,老年代使用標記清楚或標記整理算法,虛擬機實際使用這種算法。
內存分配與回收策略
對象優先在Eden上分配
GC分類
- Monior GC,新生代GC,指發生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特點,所以Monior GC很頻繁,速度也很快
- 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條。