Java 虛擬機屏蔽了與具體操作系統(tǒng)平臺相關的信息,使得 Java 語言編譯程序只需生成在 Java 虛擬機上運行的目標代碼(字節(jié)碼),就可以在多種平臺上不加修改地運行。Java 虛擬機在執(zhí)行字節(jié)碼時,實際上最終還是把字節(jié)碼解釋成具體平臺上的機器指令執(zhí)行。
Java 的優(yōu)點
- 是一門結構嚴謹、面向對象的編程語言。
- 擺脫了硬件平臺的束縛,實現(xiàn)了“一次編寫,到處運行”的理想。
- 提供了一種相對安全的內(nèi)存管理和訪問機制,避免了絕大部分的內(nèi)存泄漏和指針越界問題。
- 實現(xiàn)了熱點代碼檢測和運行時編譯及優(yōu)化,使得 Java 應用能隨著運行時間的增加而獲得更高的性能。
- 有一套完善的應用程序接口和無數(shù)的來自商業(yè)機構和開源社區(qū)的第三方類庫來幫助實現(xiàn)各種各樣的功能。
Java 平臺的邏輯結構
JVM
- JVM 是一種基于下層的操作系統(tǒng)和硬件平臺并利用軟件方法來實現(xiàn)的抽象的計算機,可以在上面執(zhí)行 Java 的字節(jié)碼程序。簡單的說,JVM 就是 Java 的虛擬機,有了 JVM 才能運行 Java 程序。
-
Java 編譯器只需面向 JVM,生成 JVM 能理解的代碼或字節(jié)碼文件。Java 源文件經(jīng)編譯器,編譯成字節(jié)碼程序,通過 JVM 將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。
JVM 自身的物理結構
class 文件的組成
- 結構信息。包括 class 文件格式版本號及各部分的數(shù)量與大小的信息。
- 元數(shù)據(jù)。對應于 Java 源碼中聲明與常量的信息。包含類/繼承的超類/實現(xiàn)的接口的聲明信息、域與方法聲明信息和常量池。
- 方法信息。對應 Java 源碼中語句和表達式對應的信息。包含字節(jié)碼、異常處理器表、求值棧與局部變量區(qū)大小、求值棧的類型記錄、調(diào)試符號信息。
類的層次關系和加載順序
類執(zhí)行機制
- JVM 是基于棧的體系結構來執(zhí)行 class 字節(jié)碼的。線程創(chuàng)建后,都會產(chǎn)生程序計數(shù)器(PC)和棧(Stack),程序計數(shù)器存放下一條要執(zhí)行的指令在方法內(nèi)的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次調(diào)用,而棧幀又是有局部變量區(qū)和操作數(shù)棧兩部分組成,局部變量區(qū)用于存放方法中的局部變量和參數(shù),操作數(shù)棧中用于存放方法執(zhí)行過程中產(chǎn)生的中間結果。
內(nèi)存區(qū)域
- Java 虛擬機在執(zhí)行 Java 程序的過程中會把他所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。
-
Java 虛擬機規(guī)范將 JVM 所管理的內(nèi)存分為以下幾個運行時數(shù)據(jù)區(qū):程序計數(shù)器、Java 虛擬機棧、本地方法棧、Java 堆、方法區(qū)。
內(nèi)存區(qū)域圖
程序計數(shù)器
- 一塊較小的內(nèi)存空間,它是當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時通過改變該計數(shù)器的值來選擇下一條需要執(zhí)行的字節(jié)碼指令,分支、跳轉、循環(huán)等基礎功能都要依賴它來實現(xiàn)。
- 每條線程都有一個獨立的的程序計數(shù)器,各線程間的計數(shù)器互不影響,因此該區(qū)域是線程私有的。
- 當線程在執(zhí)行一個 Java 方法時,該計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址,當線程在執(zhí)行的是 Native 方法(調(diào)用本地操作系統(tǒng)方法)時,該計數(shù)器的值為空。
- 該內(nèi)存區(qū)域是唯一一個在 Java 虛擬機規(guī)范中么有規(guī)定任何 OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。
Java 虛擬機棧
- 該區(qū)域也是線程私有的,它的生命周期也與線程相同。
- 虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀,棧它是用于支持續(xù)虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結構。
- 對于執(zhí)行引擎來講,活動線程中,只有棧頂?shù)臈怯行У模Q為當前棧幀,這個棧幀所關聯(lián)的方法稱為當前方法,執(zhí)行引擎所運行的所有字節(jié)碼指令都只針對當前棧幀進行操作。
- 棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法返回地址和一些額外的附加信息。
- 在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入了方法表的 Code 屬性之中。因此,一個棧幀需要分配多少內(nèi)存,不會受到程序運行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機實現(xiàn)。
Java 虛擬機棧的異常
- 在 Java 虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。如果虛擬機在動態(tài)擴展棧時無法申請到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。
- 這兩種情況存在著一些互相重疊的地方:當棧空間無法繼續(xù)分配時,到底是內(nèi)存太小,還是已使用的棧空間太大,其本質(zhì)上只是對同一件事情的兩種描述而已。在單線程的操作中,無論是由于棧幀太大,還是虛擬機棧空間太小,當棧空間無法分配時,虛擬機拋出的都是 StackOverflowError 異常,而不會得到 OutOfMemoryError 異常。而在多線程環(huán)境下,則會拋出 OutOfMemoryError 異常。
局部變量表
- 局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量,其中存放的數(shù)據(jù)的類型是編譯期可知的各種基本數(shù)據(jù)類型、對象引用(reference)和 returnAddress 類型(它指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,即在 Java 程序被編譯成 Class 文件時,就確定了所需分配的最大局部變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
- 局部變量表的容量以變量槽(Slot)為最小單位。在虛擬機規(guī)范中并沒有明確指明一個 Slot 應占用的內(nèi)存空間大小(允許其隨著處理器、操作系統(tǒng)或虛擬機的不同而發(fā)生變化),一個 Slot 可以存放一個32位以內(nèi)的數(shù)據(jù)類型:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是對象的引用類型,returnAddress 是為字節(jié)指令服務的,它執(zhí)行了一條字節(jié)碼指令的地址。對于 64 位的數(shù)據(jù)類型(long和double),虛擬機會以高位在前的方式為其分配兩個連續(xù)的 Slot 空間。
- 虛擬機通過索引定位的方式使用局部變量表,索引值的范圍是從 0 開始到局部變量表最大的 Slot 數(shù)量,對于 32 位數(shù)據(jù)類型的變量,索引 n 代表第 n 個 Slot,對于 64 位的,索引 n 代表第 n 和第 n+1 兩個 Slot。
- 在方法執(zhí)行時,虛擬機是使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果是實例方法(非static),則局部變量表中的第 0 位索引的 Slot 默認是用于傳遞方法所屬對象實例的引用,在方法中可以通過關鍵字“this”來訪問這個隱含的參數(shù)。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量 Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot。
- 局部變量表中的 Slot 是可重用的,方法體中定義的變量,作用域并不一定會覆蓋整個方法體,如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超過了某個變量的作用域,那么這個變量對應的 Slot 就可以交給其他變量使用。這樣的設計不僅僅是為了節(jié)省空間,在某些情況下 Slot 的復用會直接影響到系統(tǒng)的而垃圾收集行為。
操作數(shù)棧
- 操作數(shù)棧又常被稱為操作棧,操作數(shù)棧的最大深度也是在編譯的時候就確定了。32 位數(shù)據(jù)類型所占的棧容量為 1,64 位數(shù)據(jù)類型所占的棧容量為 2。當一個方法開始執(zhí)行時,它的操作棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內(nèi)容,也就是入棧和出棧操作。
- Java 虛擬機的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。因此我們也稱 Java 虛擬機是基于棧的,這點不同于 Android 虛擬機,Android 虛擬機是基于寄存器的。
- 基于棧的指令集最主要的優(yōu)點是可移植性強,主要的缺點是執(zhí)行速度相對會慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的優(yōu)點是執(zhí)行速度快,主要的缺點是可移植性差。
動態(tài)連接
- 每個棧幀都包含一個指向運行時常量池(在方法區(qū)中,后面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。Class 文件的常量池中存在有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用,一部分會在類加載階段或第一次使用的時候轉化為直接引用(如 final、static 域等),稱為靜態(tài)解析,另一部分將在每一次的運行期間轉化為直接引用,這部分稱為動態(tài)連接。
方法返回地址
- 當一個方法被執(zhí)行后,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個方法返回的字節(jié)碼指令或遇到了異常,并且該異常沒有在方法體內(nèi)得到處理。無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的 PC 計數(shù)器的值就可以作為返回地址,棧幀中很可能保存了這個計數(shù)器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。
- 方法退出的過程實際上等同于把當前棧幀出站,因此退出時可能執(zhí)行的操作有:恢復上層方法的局部變量表和操作數(shù)棧,如果有返回值,則把它壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整 PC 計數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
本地方法棧
- 該區(qū)域與虛擬機棧所發(fā)揮的作用非常相似,只是虛擬機棧為虛擬機執(zhí)行 Java 方法服務,而本地方法棧則為使用到的本地操作系統(tǒng)(Native)方法服務。
Java 堆
- Java Heap 是 Java 虛擬機所管理的內(nèi)存中最大的一塊,它是所有線程共享的一塊內(nèi)存區(qū)域。幾乎所有的對象實例和數(shù)組都在這類分配內(nèi)存。Java Heap 是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱為“GC堆”。
- 根據(jù) Java 虛擬機規(guī)范的規(guī)定,Java 堆可以處在物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。如果在堆中沒有內(nèi)存可分配時,并且堆也無法擴展時,將會拋出 OutOfMemoryError 異常。
方法區(qū)
- 方法區(qū)也是各個線程共享的內(nèi)存區(qū)域,它用于存儲已經(jīng)被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
- 方法區(qū)域又被稱為“永久代”,但這僅僅對于 Sun HotSpot 來講,JRockit 和 IBM J9 虛擬機中并不存在永久代的概念。
- Java 虛擬機規(guī)范把方法區(qū)描述為 Java 堆的一個邏輯部分,而且它和 Java Heap 一樣不需要連續(xù)的內(nèi)存,可以選擇固定大小或可擴展,另外,虛擬機規(guī)范允許該區(qū)域可以選擇不實現(xiàn)垃圾回收。相對而言,垃圾收集行為在這個區(qū)域比較少出現(xiàn)。該區(qū)域的內(nèi)存回收目標主要針是對廢棄常量的和無用類的回收。
- 運行時常量池是方法區(qū)的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Class文件常量池),用于存放編譯器生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。
- 運行時常量池相對于 Class 文件常量池的另一個重要特征是具備動態(tài)性,Java 語言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預置入 Class 文件中的常量池的內(nèi)容才能進入方法區(qū)的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用比較多的是 String 類的 intern()方法。
直接內(nèi)存
- 直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機規(guī)范中定義的內(nèi)存區(qū)域,它直接從操作系統(tǒng)中分配,因此不受 Java 堆大小的限制,但是會受到本機總內(nèi)存的大小及處理器尋址空間的限制,因此它也可能導致 OutOfMemoryError 異常出現(xiàn)。
- 在 JDK1.4 中新引入了 NIO 機制,它是一種基于通道與緩沖區(qū)的新 I/O 方式,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即在堆外分配內(nèi)存,這樣能在一些場景中提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數(shù)據(jù)。
- 根據(jù) Java 虛擬機規(guī)范的規(guī)定,當方法區(qū)無法滿足內(nèi)存分配需求時,將拋出 OutOfMemoryError 異常。
內(nèi)存溢出
- 在多線程情況下,給每個線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。操作系統(tǒng)為每個進程分配的內(nèi)存是有限制的,虛擬機提供了參數(shù)來控制 Java 堆和方法區(qū)這兩部分內(nèi)存的最大值,忽略掉程序計數(shù)器消耗的內(nèi)存(很小),以及進程本身消耗的內(nèi)存,剩下的內(nèi)存便給了虛擬機棧和本地方法棧,每個線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少。因此,如果是建立過多的線程導致的內(nèi)存溢出,在不能減少線程數(shù)的情況下,就只能通過減少最大堆和每個線程的棧容量來換取更多的線程。
- 內(nèi)存泄露是指分配出去的內(nèi)存沒有被回收回來,由于失去了對該內(nèi)存區(qū)域的控制,因而造成了資源的浪費。Java 中一般不會產(chǎn)生內(nèi)存泄露,因為有垃圾回收器自動回收垃圾,但這也不絕對,當我們 new 了對象,并保存了其引用,但是后面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成內(nèi)存泄露,
- 內(nèi)存溢出是指程序所需要的內(nèi)存超出了系統(tǒng)所能分配的內(nèi)存(包括動態(tài)擴展)的上限。
對象實例化分析
Object obj = new Object();
- 假設該語句出現(xiàn)在方法體中,obj 會作為引用類型(reference)的數(shù)據(jù)保存在 Java 棧的本地變量表中,而會在 Java 堆中保存該引用的實例化對象,Java 堆中還包含能查找到此對象類型數(shù)據(jù)的地址信息(如對象類型、父類、實現(xiàn)的接口、方法等),這些類型數(shù)據(jù)則保存在方法區(qū)中。
-
由于 reference 類型在 Java 虛擬機規(guī)范里面只規(guī)定了一個指向對象的引用,并沒有定義這個引用應該通過哪種方式去定位,以及訪問到 Java 堆中的對象的具體位置,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄池和直接使用指針。這兩種對象的訪問方式各有優(yōu)勢,使用句柄訪問方式的最大好處就是 reference 中存放的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而 reference 本身不需要修改。使用直接指針訪問方式的最大好處是速度快,它節(jié)省了一次指針定位的時間開銷。目前 Java 默認使用的 HotSpot 虛擬機采用的便是是第二種方式進行對象訪問的。
句柄池訪問
直接指針訪問
類文件結構
- Class 文件是一組以8位字節(jié)為基礎單位的二進制流,各個數(shù)據(jù)項目嚴格按照順序緊湊地排列在 Class 文件中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內(nèi)容幾乎全部都是程序運行的必要數(shù)據(jù)。
- 根據(jù) Java 虛擬機規(guī)范的規(guī)定,Class 文件格式采用一種類似于 C 語言結構體的偽結構來存儲,這種偽結構中只有兩種數(shù)據(jù)類型:無符號數(shù)和表。無符號數(shù)屬于基本數(shù)據(jù)類型,以 u1、u2、u4、u8 來分別代表 1、2、4、8 個字節(jié)的無符號數(shù)。表是由多個無符號數(shù)或其他表作為數(shù)據(jù)項構成的符合數(shù)據(jù)類型,所有的表都習慣性地以“_info”結尾。
magic 與 version
- 每個 Class 文件的頭 4 個字節(jié)稱為魔數(shù)(magic),它的唯一作用是判斷該文件是否為一個能被虛擬機接受的 Class 文件。它的值固定為 0xCAFEBABE。緊接著 magic 的 4 個字節(jié)存儲的是 Class 文件的次版本號和主版本號,高版本的 JDK 能向下兼容低版本的 Class 文件,但不能運行更高版本的 Class 文件。
類初始化
- 遇到 new、getstatic、putstatic、invokestatic 這四條字節(jié)碼指令時,如果類還沒有進行過初始化,則需要先觸發(fā)其初始化。生成這四條指令最常見的 Java 代碼場景是:使用 new 關鍵字實例化對象時、讀取或設置一個類的靜態(tài)字段(static)時(被 static 修飾又被 final 修飾的,已在編譯期把結果放入常量池的靜態(tài)字段除外)、以及調(diào)用一個類的靜態(tài)方法時。
- 使用 Java.lang.refect 包的方法對類進行反射調(diào)用時,如果類還沒有進行過初始化,則需要先觸發(fā)其初始化。
- 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行初始化,則需要先觸發(fā)其父類的初始化。
- 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類,虛擬機會先執(zhí)行該主類。
- 通過子類引用父類中的靜態(tài)字段,這時對子類的引用為被動引用,因此不會初始化子類,只會初始化父類:
- 常量在編譯階段會存入調(diào)用它的類的常量池中,本質(zhì)上沒有直接引用到定義該常量的類,因此不會觸發(fā)定義常量的類的初始化
- 通過數(shù)組定義來引用類,不會觸發(fā)類的初始化但是會觸發(fā)了另一個名為“LLConst”的類的初始化,它是一個由虛擬機自動生成的、直接繼承于java.lang.Object 的子類,創(chuàng)建動作由字節(jié)碼指令 newarray 觸發(fā),很明顯,這是一個對數(shù)組引用類型的初初始化,而該數(shù)組中的元素僅僅包含一個對 Const 類的引用,并沒有對其進行初始化。如果我們加入對 con 數(shù)組中各個 Const 類元素的實例化代碼,便會觸發(fā) Const 類的初始化
- 接口也有初始化過程,在接口中不能使用“static{}”語句塊,但編譯器仍然會為接口生成類構造器,用于初始化接口中定義的成員變量(實際上是 static final 修飾的全局常量)。二者在初始化時最主要的區(qū)別是:當一個類在初始化時,要求其父類全部已經(jīng)初始化過了,但是一個接口在初始化時,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量),才會初始化該父接口。這點也與類初始化的情況很不同,調(diào)用類中的 static final 常量時并不會 觸發(fā)該類的初始化,但是調(diào)用接口中的 static final 常量時便會觸發(fā)該接口的初始化。
類加載機制 ???
多態(tài)性實現(xiàn)機制——靜態(tài)分派與動態(tài)分派 ???
Java 語法糖
語法糖(Syntactic Sugar),也稱糖衣語法,是由英國計算機學家 Peter.J.Landin 發(fā)明的一個術語,指在計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。
Java 中最常用的語法糖主要有泛型、變長參數(shù)、條件編譯、自動拆裝箱、內(nèi)部類等。虛擬機并不支持這些語法,它們在編譯階段就被還原回了簡單的基礎語法結構,這個過程成為解語法糖。
泛型是 JDK1.5 之后引入的一項新特性,Java 語言在還沒有出現(xiàn)泛型時,只能通過 Object 是所有類型的父類和類型強制轉換這兩個特點的配合來實現(xiàn)泛型的功能,這樣實現(xiàn)的泛型功能要在程序運行期才能知道 Object 真正的對象類型,在 javac 編譯期,編譯器無法檢查這個 Object 的強制轉型是否成功,這便將一些風險轉接到了程序運行期中。Java 語言在 JDK1.5 之后引入的泛型實際上只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)被替換為了原來的原生類型,并且在相應的地方插入了強制轉型代碼,所以泛型技術實際上是 Java 語言的一顆語法糖,Java 語言中的泛型實現(xiàn)方法稱為類型擦除,基于這種方法實現(xiàn)的泛型被稱為偽泛型。
javac 編譯
- javac 編譯器稱為前端編譯器,將.java文件編譯成為.class文件。相對應的還有后端編譯器,它在程序運行期間將字節(jié)碼轉變成機器碼(現(xiàn)在的 Java 程序在運行時基本都是解釋執(zhí)行加編譯執(zhí)行),如 HotSpot 虛擬機自帶的 JIT(Just In Time Compiler)編譯器(分 Client 端和 Server 端)。
詞法、語法分析
- 詞法分析是將源代碼的字符流轉變?yōu)闃擞洠═oken)集合。單個字符是程序編寫過程中的的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符等都可以成為標記,比如整型標志 int 由三個字符構成,但是它只是一個標記,不可拆分。
- 語法分析是根據(jù)Token序列來構造抽象語法樹的過程。抽象語法樹是一種用來描述程序代碼語法結構的樹形表示方式,語法樹的每一個節(jié)點都代表著程序代碼中的一個語法結構,如 bao、類型、修飾符、運算符等。經(jīng)過這個步驟后,編譯器就基本不會再對源碼文件進行操作了,后續(xù)的操作都建立在抽象語法樹之上。
填充符號表
- 完成了語法分析和詞法分析之后,下一步就是填充符號表的過程。符號表是由一組符號地址和符號信息構成的表格。符號表中所登記的信息在編譯的不同階段都要用到,在語義分析中,符號表所登記的內(nèi)容將用于語義檢查和產(chǎn)生中間代碼,在目標代碼生成階段,黨對符號名進行地址分配時,符號表是地址分配的依據(jù)。
語義分析
- 語法樹能表示一個結構正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務是讀結構上正確的源程序進行上下文有關性質(zhì)的審查。語義分析過程分為標注檢查和數(shù)據(jù)及控制流分析兩個步驟:
- 標注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量和賦值之間的數(shù)據(jù)類型是否匹配等。
- 數(shù)據(jù)及控制流分析是對程序上下文邏輯更進一步的驗證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。
字節(jié)碼生成
- 字節(jié)碼生成是 javac 編譯過程的最后一個階段。字節(jié)碼生成階段不僅僅是把前面各個步驟所生成的信息轉化成字節(jié)碼寫到磁盤中,編譯器還進行了少量的代碼添加和轉換工作。 實例構造器()方法和類構造器()方法就是在這個階段添加到語法樹之中的(這里的實例構造器并不是指默認的構造函數(shù),而是指我們自己重載的構造函數(shù),如果用戶代碼中沒有提供任何構造函數(shù),那編譯器會自動添加一個沒有參數(shù)、訪問權限與當前類一致的默認構造函數(shù),這個工作在填充符號表階段就已經(jīng)完成了)。
JIT 編譯
- Java 程序最初是僅僅通過解釋器解釋執(zhí)行的,即對字節(jié)碼逐條解釋執(zhí)行,這種方式的執(zhí)行速度相對會比較慢,尤其當某個方法或代碼塊運行的特別頻繁時,這種方式的執(zhí)行效率就顯得很低。于是后來在虛擬機中引入了 JIT 編譯器(即時編譯器),當虛擬機發(fā)現(xiàn)某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“Hot Spot Code”(熱點代碼),為了提高熱點代碼的執(zhí)行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優(yōu)化,完成這項任務的正是 JIT 編譯器。
- HotSpot 虛擬機中內(nèi)置了兩個JIT編譯器:Client Complier 和 Server Complier,分別用在客戶端和服務端,目前主流的 HotSpot 虛擬機中默認是采用解釋器與其中一個編譯器直接配合的方式工作。
運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:
- 被多次調(diào)用的方法。
- 被多次調(diào)用的循環(huán)體。
目前主要的熱點判定方式
- 基于采樣的熱點探測:采用這種方法的虛擬機會周期性地檢查各個線程的棧頂,如果發(fā)現(xiàn)某些方法經(jīng)常出現(xiàn)在棧頂,那這段方法代碼就是“熱點代碼”。這種探測方法的好處是實現(xiàn)簡單高效,還可以很容易地獲取方法調(diào)用關系,缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的外界因素的影響而擾亂熱點探測。
- 基于計數(shù)器的熱點探測:采用這種方法的虛擬機會為每個方法,甚至是代碼塊建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閥值,就認為它是“熱點方法”。這種統(tǒng)計方法實現(xiàn)復雜一些,需要為每個方法建立并維護計數(shù)器,而且不能直接獲取到方法的調(diào)用關系,但是它的統(tǒng)計結果相對更加精確嚴謹。
在 HotSpot 虛擬機的熱點判定方式
- 在 HotSpot 虛擬機中使用的是基于計數(shù)器的熱點探測方法,因此它為每個方法準備了兩個計數(shù)器:方法調(diào)用計數(shù)器和回邊計數(shù)器。
- 方法調(diào)用計數(shù)器用來統(tǒng)計方法調(diào)用的次數(shù),在默認設置下,方法調(diào)用計數(shù)器統(tǒng)計的并不是方法被調(diào)用的絕對次數(shù),而是一個相對的執(zhí)行頻率,即一段時間內(nèi)方法被調(diào)用的次數(shù)。
- 回邊計數(shù)器用于統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù)(準確地說,應該是回邊的次數(shù),因為并非所有的循環(huán)都是回邊),在字節(jié)碼中遇到控制流向后跳轉的指令就稱為“回邊”。
- 在確定虛擬機運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的閥值,當計數(shù)器的值超過了閥值,就會觸發(fā)JIT編譯。觸發(fā)了 JIT 編譯后,在默認設置下,執(zhí)行引擎并不會同步等待編譯請求完成,而是繼續(xù)進入解釋器按照解釋方式執(zhí)行字節(jié)碼,直到提交的請求被編譯器編譯完成為止(編譯工作在后臺線程中進行)。當編譯工作完成后,下一次調(diào)用該方法或代碼時,就會使用已編譯的版本。
對象引用
Java 中的垃圾回收一般是在 Java 堆中進行,因為堆中幾乎存放了 Java 中所有的對象實例。在 JDK1.2 之前,Java 中的引用定義很很純粹:如果 reference 類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊數(shù)據(jù)代表著一個引用。但在 JDK1.2 之后,Java 對引用的概念進行了擴充,將其分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,引用強度依次減弱。
- 強引用:如“Object obj = new Object()”,這類引用是 Java 程序中最普遍的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用:它用來描述一些可能還有用,但并非必須的對象。在系統(tǒng)內(nèi)存不夠用時,這類引用關聯(lián)的對象將被垃圾收集器回收。JDK1.2 之后提供了 SoftReference 類來實現(xiàn)軟引用。
- 弱引用:它也是用來描述非需對象的,但它的強度比軟引用更弱些,被弱引用關聯(lián)的對象只能生存島下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關聯(lián)的對象。在 JDK1.2 之后,提供了 WeakReference 類來實現(xiàn)弱引用。
- 虛引用:最弱的一種引用關系,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯(lián)的唯一目的是希望能在這個對象被收集器回收時收到一個系統(tǒng)通知。JDK1.2 之后提供了 PhantomReference 類來實現(xiàn)虛引用。
垃圾對象的判定
引用計數(shù)算法
- 給對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值就加 1,當引用失效時,計數(shù)器值就減1,任何時刻計數(shù)器都為 0 的對象就是不可能再被使用的。
- 引用計數(shù)算法的實現(xiàn)簡單,判定效率也很高,在大部分情況下它都是一個不錯的選擇,當 Java 語言并沒有選擇這種算法來進行垃圾回收,主要原因是它很難解決對象之間的相互循環(huán)引用問題。
根搜索算法
- 這種算法的基本思路是通過一系列名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,就證明此對象是不可用的。Java 和 C# 中都是采用根搜索算法來判定對象是否存活的。
- 在根搜索算法中,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標記過程:如果對象在進行根搜索后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執(zhí)行 finalize()方法。當對象沒有覆蓋 finalize()方法,或 finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為沒有必要執(zhí)行。如果該對象被判定為有必要執(zhí)行 finalize()方法,那么這個對象將會被放置在一個名為 F-Queue 隊列中,并在稍后由一條由虛擬機自動建立的、低優(yōu)先級的 Finalizer 線程去執(zhí)行 finalize()方法。finalize()方法是對象逃脫死亡命運的最后一次機會(因為一個對象的 finalize()方法最多只會被系統(tǒng)自動調(diào)用一次),稍后 GC 將對 F-Queue 中的對象進行第二次小規(guī)模的標記,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中讓該對象重引用鏈上的任何一個對象建立關聯(lián)即可。而如果對象這時還沒有關聯(lián)到任何鏈上的引用,那它就會被回收掉。
垃圾收集算法
標記—清除算法
- 標記—清除算法是最基礎的收集算法,它分為“標記”和“清除”兩個階段:首先標記出所需回收的對象,在標記完成后統(tǒng)一回收掉所有被標記的對象,它的標記過程其實就是前面的根搜索算法中判定垃圾對象的標記過程。(會造成大量的內(nèi)存碎片)
標記—整理算法
- 復制算法比較適合于新生代,在老年代中,對象存活率比較高,如果執(zhí)行較多的復制操作,效率將會變低,所以老年代一般會選用其他算法,如標記—整理算法。該算法標記的過程與標記—清除算法中的標記過程一樣,但對標記后出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。(不會產(chǎn)生內(nèi)存碎片,成本相對較高)
分代收集
- 當前商業(yè)虛擬機的垃圾收集 都采用分代收集,它根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊,一般是把 Java 堆分為新生代和老年代。在新生代中,每次垃圾收集時都會發(fā)現(xiàn)有大量對象死去,只有少量存活,因此可選用復制算法來完成收集,而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。
垃圾收集器
內(nèi)存的分配策
- 對象優(yōu)先在 Eden 分配。
- 大對象直接進入老年代。
- 長期存活的對象將進入老年代。
垃圾回收策略
- 新生代 GC(Minor GC):發(fā)生在新生代的垃圾收集動作,因為 Java 對象大多都具有朝生夕滅的特性,因此Minor GC 非常頻繁,一般回收速度也比較快。
- 老年代 GC(Major GC/Full GC):發(fā)生在老年代的 GC,出現(xiàn)了 Major GC,經(jīng)常會伴隨至少一次 Minor GC。由于老年代中的對象生命周期比較長,因此 Major GC 并不頻繁,一般都是等待老年代滿了后才進行 Full GC,而且其速度一般會比 Minor GC 慢 10 倍以上。另外,如果分配了 Direct Memory,在老年代中進行 Full GC時,會順便清理掉 Direct Memory 中的廢棄對象。
性能調(diào)優(yōu)
- 我們可以通過給 Java 虛擬機分配超大堆(前提是物理機的內(nèi)存足夠大)來提升服務器的響應速度,但分配超大堆的前提是有把握把應用程序的 Full GC 頻率控制得足夠低,因為一次 Full GC 的時間造成比較長時間的停頓。控制 Full GC 頻率的關鍵是保證應用中絕大多數(shù)對象的生存周期不應太長,尤其不能產(chǎn)生批量的、生命周期長的大對象,這樣才能保證老年代的穩(wěn)定。
- Direct Memory 在堆內(nèi)存外分配,而且二者均受限于物理機內(nèi)存,且成負相關關系,因此分配超大堆時,如果用到了 NIO 機制分配使用了很多的 Direct Memory,則有可能導致 Direct Memory 的 OutOfMemoryError 異常,這時可以通過 -XX:MaxDirectMemorySize 參數(shù)調(diào)整 Direct Memory 的大小。