java開發之JVM基礎知識分享
虛擬機器執行機制
JVM執行在作業系統上,不與硬體裝置直接互動。
Java程式執行流程:Java原始碼檔案( Hello.java)被編譯器編譯成位元組碼檔案( Hello.class),位元組碼檔案被JVM中的直譯器編譯成機器碼在不同作業系統上執行。
Java程式具體執行過程:
- Java原始檔被編譯成位元組碼檔案
- JVM將位元組碼檔案通過JVM內部直譯器編譯成與作業系統對應的機器碼
- 機器碼呼叫相應作業系統的Native Method庫執行相應方法
Java跨平臺的原因:每種作業系統的直譯器都是不同的,但基於直譯器實現的虛擬機器是相同的。
虛擬機器例項生命週期描述:在一個Java程序開始執行後,虛擬機器就開始例項化,有多個程序啟動就會例項化多個虛擬機器例項。程序退出或關閉,則虛擬機器例項銷燬,
虛擬機器內部結構
JVM包括一個類載入器子系統(Class Loader Subsystem)、執行時資料區(Runtime Data Area)、執行引擎(Engine)和本地介面庫(Nateive Interface Library)。
本地介面庫呼叫本地方法庫(Native Method Library)與OS互動。
JVM內部結構圖如下:
其中:
- 類載入器子系統用於將編譯好的位元組碼檔案( .class)載入到JVM。
- 執行時資料區用於儲存在JVM執行過程中產生的資料,包括程式計數器(PC)、方法區(Method Area)、本地方法區(Native Method Area)、虛擬機器棧(JVM Stack)、虛擬機器堆(JVM Heap)。
- 執行引擎包括即時編譯器(JIT Compiler)和垃圾回收器(Garbage Collection)。
- 即時編譯器用於將位元組碼編譯成具體的機器碼。
- 垃圾回收器用於管理記憶體,回收在執行過程中不再使用的物件。
- 本地介面庫用於呼叫作業系統的本地方法庫完成具體的指令操作。
虛擬機器與多執行緒
在多核作業系統上,JVM允許在一個程序內同時併發執行多個執行緒。
JVM中的執行緒與作業系統中的執行緒是相互對應的。
JVM執行緒的排程是交給作業系統負責的。
有些程式語言有協程的概念,如Golang的併發,協程可以粗略地看成是輕量級的執行緒,一個協程並非對應一個作業系統執行緒,而是多個協程對應一個作業系統執行緒,協程之間通過排程器協調。
這種設計有以下的好處:
輕量級,資源佔用較少。
排程是基於語言層面,減少作業系統執行緒排程的開銷。
從JVM角度看,Java執行緒的執行流程:
- 準備:完成JVM執行緒的本地儲存、緩衝區分配、同步物件、棧、程式計數器等初始化工作。
- 建立:呼叫作業系統介面建立一個與之對應的原生執行緒。
- 排程:作業系統負責排程所有執行緒,併為其分配CPU時間片。
- 執行:在原生執行緒初始化完畢時,呼叫Java執行緒的 run()方法執行執行緒邏輯。
- 結束:在 run()方法邏輯執行完畢後,釋放原生執行緒和Java執行緒所對應的資源。
- 回收:在Java執行緒執行結束時,原生執行緒隨之被回收。
在JVM後臺執行的執行緒主要有:
- 虛擬機器執行緒(JVM Thread):虛擬機器執行緒在JVM到達安全點(Safe Point)時出現。
- 週期性任務執行緒:通過定時器排程執行緒來實現週期性操作的執行。
- GC執行緒:GC執行緒支援JVM中不同的垃圾回收活動。
- 編譯器執行緒:編譯器執行緒在執行時將位元組碼動態編譯成本地平臺機器碼,是JVM跨平臺的具體實現。
- 訊號分發執行緒:接收發送到JVM的訊號並呼叫JVM方法。
虛擬機器記憶體區域
JVM的記憶體區域分為:
- 執行緒私有區域:執行緒私有區域包括的元件有程式計數器、虛擬機器棧、本地方法區。
- 執行緒共享區域:虛擬機器堆、方法區。
- 直接記憶體
執行緒私有區域的生命週期與執行緒相同,隨執行緒的啟動而建立,隨執行緒的結束而銷燬。
在JVM中,每個Java執行緒與作業系統本地執行緒直接對映,因此這部分記憶體區域的存在與否和本地執行緒的啟動和銷燬對應。
執行緒共享區域隨虛擬機器的啟動而建立,隨虛擬機器的關閉而銷燬。
直接記憶體又稱為對外記憶體,直接記憶體不是JVM執行時資料區的一部分,但在併發程式設計中被頻繁呼叫。
JDK的NIO模組提供的基於 Channel與 Buffer的I/O操作就是基於堆外記憶體實現的,NIO模組通過呼叫Native Method Library直接在作業系統上分配堆外記憶體,然後使用 DirectByteBuffer物件作為這塊記憶體的引用對記憶體進行操作,Java程序可以通過堆外記憶體技術避免在Java堆和Native堆中來回複製資料帶來的資源佔用和效能消耗,因此堆外記憶體在高併發應用場景下被廣泛使用(Netty、Flink、HBase、Hadoop都有用到堆外記憶體)。
程式計數器
程式計數器(PC)屬於執行緒私有區域,程式計數器是唯一無記憶體溢位(Out of Memory)問題的區域。
程式計數器是一塊很小的記憶體空間,用於儲存當前執行的執行緒所執行的位元組碼的行號指示器。
每個執行中的執行緒都有一個獨立的程式計數器,在方法正在執行時,該方法的程式計數器記錄的是實時虛擬機器位元組碼指令的地址。
注:如果該方法執行的是 native方法,則程式計數器的值為空(Undefined)。
虛擬機器棧
虛擬機器棧(JVM Stack)屬於執行緒私有區域,描述Java方法的執行過程。
虛擬機器棧是描述Java方法的執行過程的記憶體模型,它在當前棧幀(Stack Frame)中主要儲存了以下資訊:
- 區域性變量表
- 運算元棧
- 動態連結
- 方法出口
同時,棧幀用來儲存部分執行時資料及其資料結構,處理動態連結(Dynamic Linking)方法的返回值和異常分派(Dispatch Exception)。
棧幀:棧幀用來記錄方法的執行過程。
- 方法被執行時,虛擬機器會為其建立一個與之對應的棧幀。
- 虛擬機器棧中的入棧操作:方法的執行。
- 虛擬機器棧中的出棧操作:方法的返回。
- 無論方法是正常執行完成,還是異常(丟擲了在方法內未被捕獲的異常)退出,都認為方法執行結束。
執行緒執行及棧幀的變化過程如下:
執行緒1在CPU1上執行,執行緒2在CPU2上執行,在CPU資源不夠時其他執行緒將處於等待狀態(圖中的執行緒N),等待獲取CPU時間片。而線上程內部,每個方法的執行和返回都對應一個棧幀的入棧和出棧,每個執行中的執行緒當前只有一個棧幀處於活動狀態。
本地方法區
本地方法區(Native Method Area)和虛擬機器棧的作用類似,區別是虛擬機器棧是為執行Java方法服務的,本地方法區是為Native方法服務的。
虛擬機器堆
虛擬機器堆(JVM Heap),也稱為執行時資料區,虛擬機器堆是執行緒共享的。
在JVM執行過程中建立的物件和產生的資料都被儲存在堆中,堆是被執行緒共享的記憶體區域,也是垃圾回收器進行垃圾回收的最主要的記憶體區域。
由於現代JVM採用分代收集演算法,因此Java堆從GC(Garbage Collection,垃圾回收)的角度還可以細分為:新生代、老年代和永久代。
方法區
方法區(Method Area),也被稱為永久代,用於儲存常量、靜態變數、類資訊、JIT編譯後的機器碼、執行時常量池等資料。
JVM把GC分代收集擴充套件至方法區,即使用Java堆的永久代來實現方法區,這樣JVM的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體。永久帶的記憶體回收主要針對常量池的回收和類的解除安裝,因此可回收的物件很少。
常量被儲存在執行時常量池(Runtime Constant Pool)中,是方法區的一部分。靜態變數也屬於方法區的一部分。在類資訊(Class檔案)中不但儲存了類的版本、欄位、方法、介面等描述資訊,還儲存了常量資訊。
在即時編譯後,程式碼的內容將在執行階段(類載入完成後)被儲存在方法區的執行時常量池中。Java虛擬機器對Class檔案每一部分的格式都有明確的規定,只有符合JVM規範的Class檔案才能通過虛擬機器的檢查,然後被裝載、執行。
虛擬機器執行時記憶體
JVM的執行時記憶體也叫做JVM堆,從GC的角度可以將JVM堆分為新生代、老年代和永久代。
其中,新生代預設佔1/3堆空間,老年代預設佔2/3堆空間。
新生代又分為Eden區、ServivorFrom和ServivorTo區。
- Eden區預設佔8/10新生代空間。
- ServivorForm區和ServivorTo區預設分別佔1/10新生代空間。
JVM堆分代分割槽的結構如下:
JVM新建立的物件(除了大物件)都會被存放在新生代,預設佔1/3堆記憶體空間。
由於JVM會頻繁建立物件,所以新生代會頻繁出發MinorGC進行垃圾回收。
新生代
新生代分為Eden區(8/10新生代空間)、ServivorFrom區(1/10新生代空間)、ServivorTo區(1/10新生代空間)。
Eden區:Java新建立的物件首先會被存放在Eden區,如果新建立的物件屬於大物件,則直接將其分配到老年代。
- 大物件的定義和具體的JVM版本、堆大小和垃圾回收策略有關,一般為 2KB~128KB,可通過 XX:PretenureSizeThreshold設定其大小。
- 在Eden區的記憶體空間不足時會觸發MinorGC,對新生代進行一次垃圾回收。
ServivorTo區:保留上一次MinorGC時的倖存者。
ServivorFrom區:將上一次MinorGC時的倖存者作為這一次MinorGC的被掃描者。
新生代的GC過程叫做MinorGC,採用複製演算法實現,具體過程如下:
- 將Eden區和ServivorFrom區中存活的物件複製到ServivorTo區,如果某物件的年齡達到老年代的標準,則將其複製到老年代,同時將這些物件年齡加1。
- 物件晉升老年代的標準由 XX:MaxTenuringThreshold設定,預設為 15。
- 如果ServivorTo區的記憶體空間不夠,則也直接將其複製到老年代。
- 如果物件屬於大物件,也直接將其複製到老年代。
- 清空Eden區和ServivorFrom區物件。
- 將ServivorTo區和ServivorFrom區互換,原來的ServivorTo區成為下一次GC時的ServivorFrom區。
老年代
老年代主要存放長生命週期物件和大物件。
老年代的GC過程叫做MajorGC,在老年代,物件比較穩定,MajorGC不會被頻繁觸發。
在進行MajorGC之前,JVM會進行一次MinorGC,在MinorGC過後仍然出現老年代空間不足或無法找到足夠大的連續記憶體空間分配給新建立的大物件時,會觸發MajorGC進行垃圾回收活動,釋放JVM的記憶體空間。
MajorGC採用標記清除演算法,該演算法首先會掃描所以物件並標記存活的物件,然後回收未被標記的物件,並釋放記憶體空間。
因為要先掃描老年代的所有物件再回收,所以MajorGC的耗時比較長,MajorGC的標記清除演算法容易產生記憶體碎片。
在老年代沒有記憶體空間可分配時,會丟擲OOM異常。
永久代
永久代指記憶體的永久儲存區域,主要存放 Class和 Meta(元資料)的資訊。
Class在類載入時被放入永久代。
永久代和老年代、新生代不同,GC不會在程式執行期間對永久代的記憶體進行清理,這也導致了永久代的記憶體會隨著載入的Class檔案的增加而增加,在載入的Class檔案過多時會丟擲Out OfMemory異常,比如Tomcat引用Jar檔案過多導致JVM記憶體不足而無法啟動。
需要注意的是,在Java 8中永久代已經被元資料區(也叫作元空間)取代。
元資料區的作用和永久代類似,二者最大的區別在於:元資料區並沒有使用虛擬機器的記憶體,而是直接使用作業系統的本地記憶體。
因此,元空間的大小不受JVM記憶體的限制,只和作業系統的記憶體有關。
在Java 8中,JVM將類的元資料放入本地記憶體(Native Memory)中,將常量池和類的靜態變數放入Java堆中,這樣JVM能夠載入多少元資料資訊就不再由JVM的最大可用記憶體(MaxPermSize)空間決定,而由作業系統的實際可用記憶體空間決定。
垃圾回收與演算法
確定垃圾
Java採用引用計數法和可達性分析來確定物件是否需要被回收。
- 引用計數法容易產生迴圈引用問題。
- 可達性分析通過根搜尋演算法(GC Roots Tracing)來實現。
根搜尋演算法以一系列GC Roots的點作為起點向下搜尋,在一個物件到任何 GC Roots都沒有引用鏈相連時,說明其已死亡。
- 根搜尋演算法主要針對棧中的引用、方法區中的靜態引用和JNI中的引用展開分析。
引用計數法
在Java中如果要操作物件,就必須先獲取該物件的引用,因此可以通過引用計數法來判斷一個物件是否可以被回收。
在為物件新增一個引用時,引用計數加1;在為物件刪除一個引用時,引進計數減1;如果一個物件的引用計數為0,則表示此刻該物件沒有被引用,可以被回收。
引用計數法容易產生迴圈引用問題。
迴圈引用指兩個物件相互引用,導致它們的引用一直存在,而不能被回收。
可達性分析
為了解決引用計數法的迴圈引用問題,Java還採用了可達性分析來判斷物件是否可以被回收。
可達性分析的過程:
- 首先定義一些GC Roots物件,然後以這些GC Roots物件作為起點向下搜尋,如果在GC roots和一個物件之間沒有可達路徑,則稱該物件是不可達的。
- 不可達物件要經過至少兩次標記才能判定其是否可以被回收,如果在兩次標記後該物件仍然是不可達的,則將被垃圾收集器回收。
常用垃圾回收演算法
Java中常用的垃圾回收演算法有:
- 標記清除(Mark-Sweep)演算法
- 複製(Copying)演算法
- 標記整理(Mark Compact)演算法
- 分代收集(Generational Collecting)演算法
標記清除演算法
標記清除演算法是基礎的垃圾回收演算法,其過程分為標記和清除階段。
在標記階段標記所以需要回收的物件,在清除階段清除可回收的物件並釋放其所佔用的記憶體空間。
由於標記清除演算法在清理物件所佔用的記憶體空間後並沒有重新整理可用的記憶體空間,因此如果記憶體中可被回收的小物件居多,則會引起記憶體碎片化的問題,繼而引起大物件無法獲得連續可用空間的問題。
複製演算法
複製演算法是為了解決標記清除演算法記憶體碎片化的問題而設計的。
複製演算法的基本原理:
- 首先將記憶體劃分為兩塊大小相等的記憶體區域,即區域1和區域2,新生成的物件都被存放在區域1中。
- 在區域1內的物件儲存滿後會對區域1進行一次標記,並將標記後仍然存活的物件全部複製到區域2中,這時區域1將不存在任何存活的物件,直接清理整個區域1的記憶體即可。
複製演算法的記憶體清理效率高且易於實現,但由於同一時刻只有一個記憶體區域可用,即可用的記憶體空間被壓縮到原來的一半,因此存在大量的記憶體浪費。
同時,在系統中有大量長時間存活的物件時,這些物件將在記憶體區域1和記憶體區域2之間來回複製而影響系統的執行效率。
因此,該演算法只在物件為“朝生夕死”狀態時執行效率較高。
標記整理演算法
標記整理演算法結合了標記清除演算法和複製演算法的優點,其標記階段和標記清除演算法的標記階段相同,在標記完成後將存活的物件移到記憶體的另一端,然後清除該端的物件並釋放記憶體。
分代收集演算法
無論是標記清除演算法、複製演算法還是標記整理演算法,都無法對所有型別(長生命週期、短生命週期、大物件、小物件)的物件都進行垃圾回收。
因此,針對不同的物件型別,JVM採用了不同的垃圾回收演算法,該演算法被稱為分代收集演算法。
分代收集演算法根據物件的不同型別將記憶體劃分為不同的區域,JVM將堆劃分為新生代和老年代。
新生代主要存放新生成的物件,其特點是物件數量多但是生命週期短,在每次進行垃圾回收時都有大量的物件被回收。
老年代主要存放大物件和生命週期長的物件,因此可回收的物件相對較少。
因此,JVM根據不同的區域物件的特點選擇了不同的演算法。
目前,大部分JVM在新生代都採用了複製演算法,因為在新生代中每次進行垃圾回收時都有大量的物件被回收,需要複製的物件(存活的物件)較少,不存在大量的物件在記憶體中被來回複製的問題,因此採用複製演算法能安全、高效地回收新生代大量的短生命週期的物件並釋放記憶體。
JVM將新生代進一步劃分為一塊較大的Eden區和兩塊較小的Servivor區,Servivor區又分為ServivorFrom區和ServivorTo區。
JVM在執行過程中主要使用Eden區和ServivorFrom區,進行垃圾回收時會將在Eden區和ServivorFrom區中存活的物件複製到ServivorTo區,然後清理Eden區和ServivorFrom區的記憶體空間。
老年代主要存放生命週期較長的物件和大物件,因而每次只有少量非存活的物件被回收,因而在老年代採用標記清除演算法。
在JVM中還有一個區域,即方法區的永久代,永久代用來儲存Class類、常量、方法描述等。
在永久代主要回收廢棄的常量和無用的類。
JVM記憶體中的物件主要被分配到新生代的Eden區和ServivorFrom區,在少數情況下會被直接分配到老年代。
在新生代的Eden區和ServivorFrom區的記憶體空間不足時會觸發一次GC,該過程被稱為MinorGC。
在MinorGC後,在Eden區和ServivorFrom區中存活的物件會被複制到ServivorTo區,然後Eden區和ServivorFrom區被清理。
如果此時在ServivorTo區無法找到連續的記憶體空間儲存某個物件,則將這個物件直接儲存到老年代。
若Servivor區的物件經過一次GC後仍然存活,則其年齡加1。
在預設情況下,物件在年齡達到15時,將被移到老年代。
引用型別
在Java中,一切皆物件,物件的操作是通過該物件的引用(Reference)實現的。
在Java中,引用型別有4種:
- 強引用
- 軟引用
- 弱引用
- 虛引用
強引用
在Java中最常見的就是強引用。
強引用:在把一個物件賦給一個引用變數時,這個引用變數就是一個強引用。
有強引用的物件一定為可達性狀態,所以不會被垃圾回收機制回收。
因此,強引用是造成Java記憶體洩漏(Memory Link)的主要原因。
軟引用
軟引用:軟引用通過 SoftReference類實現。
如果一個物件只有軟引用,則在系統記憶體空間不足時該物件將被回收。
弱引用
弱引用:弱引用通過 WeakReference類實現。
如果一個物件只有弱引用,則在垃圾回收過程中一定會被回收。
虛引用
虛引用:虛引用通過 PhantomReference類實現。
虛引用和引用佇列聯合使用,主要用於跟蹤物件的垃圾回收狀態。
分代收集演算法
JVM根據物件存活週期的不同將記憶體劃分為新生代、老年代和永久代,並根據各年代的特點分別採用不同的GC演算法。
新生代與複製演算法
新生代主要儲存短生命週期的物件,因此在垃圾回收的標記階段會標記大量已死亡的物件及少量存活的物件,因此只需要選用複製演算法將少量存活的物件複製到記憶體的另一端並清理原區域的記憶體即可。
老年代與標記整理演算法
老年代主要存放長生命週期的物件和大物件,可回收的物件一般較少,因此JVM採用標記整理演算法進行垃圾回收,直接釋放死亡狀態的物件所佔用的記憶體空間即可。
分割槽收集演算法
分割槽演算法將整個堆空間劃分為連續的大小不同的小區域,對每個小區都單獨進行記憶體使用和垃圾回收,這樣做的好處是可以根據每個小區域記憶體的大小靈活使用和釋放記憶體。
分割槽收集演算法可以根據系統可接受的停頓時間,每個都快速回收若干個小區域的記憶體,以縮短垃圾回收系統停頓的時間,最後以多次並行累加的方式逐步完成整個記憶體區域的垃圾回收。
如果垃圾回收機制一次回收整個堆記憶體,則需要更長的系統停頓時間,長時間的系統停頓將影響系統執行的穩定性。
垃圾收集器
Java堆記憶體分為新生代和老年代。
新生代主要儲存短生命週期的物件,適合使用複製演算法進行垃圾回收。
老年代主要儲存長生命週期物件和大物件,適合使用標記整理演算法進行垃圾回收。
JVM針對新生代和老年代分別提供了多種不同的垃圾收集器,針對新生代提供的垃圾收集器有 SerialOld、 ParallelOld、 CMS,還有針對不同區域的 G1分割槽收集演算法。
Serial
Serial:單執行緒、複製演算法。
Serial垃圾收集器基於複製演算法實現,它是一個單執行緒收集器,在它正在進行垃圾收集時,必須暫停其他所以工作執行緒,直到垃圾收集結束。
Serial垃圾收集器採用了複製演算法,簡單、搞笑,對於單CPU執行環境來說,沒有執行緒互動開銷,可以獲得最高的單執行緒垃圾收集效率,因此Serial垃圾收集器是JVM執行在Client模式下的新生代的預設垃圾收集器。
ParNew
ParNew:多執行緒、複製演算法。
ParNew垃圾收集器是Serial垃圾收集器的多執行緒實現,同樣採用了複製演算法,它採用多執行緒模式工作,除此之外和Serial收集器幾乎一樣。
ParNew垃圾收集器在垃圾收集過程中會暫停所有其他工作執行緒,是Java虛擬機器執行在Server模式下的新生代的預設垃圾收集器。
ParNew垃圾收集器預設開啟與CPU同等數量的執行緒進行垃圾回收,在Java應用啟動時可通過 -XX:ParallelGCThreads引數調節ParNew垃圾收集器的工作執行緒數。
Parallel Scavenge
Parallel Scavenge:多執行緒、複製演算法。
Parallel Scavenge收集器是為提高新生代垃圾收集效率而設計的垃圾收集器,基於多執行緒複製演算法實現,在系統吞吐量上有很大的優化,可以更高效地利用CPU儘快完成垃圾回收任務。
Parallel Scavenge通過自適應調節策略提高系統吞吐量,提供了三個引數用於調節、控制垃圾回收的停頓時間及吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis引數,控制吞吐量大小的 -XX:GCTimeRatio引數和控制自適應調節策略開啟與否的 UseAdaptiveSizePolicy引數。
Serial Old
Serial Old:單執行緒、標記整理演算法。
Serial Old垃圾收集器是Serial垃圾收集器的老年代實現,同Serial一樣採用單執行緒執行,不同的是,Serial Old針對老年代長生命週期的特點基於標記整理演算法實現。
Serial Old垃圾收集器是JVM執行在Client模式下的老年代的預設垃圾收集器。
新生代的Serial垃圾收集器和老年代的Serial Old垃圾收集器可搭配使用,分別針對JVM的新生代和老年代進行垃圾回收,其垃圾收集過程如圖所示。
在新生代採用Serial垃圾收集器基於複製演算法進行垃圾回收,未被其回收的物件在老年代被Serial Old垃圾收集器基於標記整理演算法進行垃圾回收。
Parallel Old
Parallel Old:多執行緒、標記整理演算法。
Parallel Old垃圾收集器採用多執行緒併發進行垃圾回收,它根據老年代長生命週期的特點,基於多執行緒的標記整理演算法實現。
Parallel Old垃圾收集器在設計上優先考慮系統吞吐量,其次考慮停頓時間等因素,如果系統對吞吐量的要求較高,則可以優先考慮新生代的Parallel Scavenge垃圾收集器和老年代的Parallel Old垃圾收集器的配合使用。
新生代的Parallel Scavenge垃圾收集器和老年代的Parallel Old垃圾收集器的搭配執行過程如圖。
新生代基於Parallel Scavenge垃圾收集器的複製演算法進行垃圾回收,老年代基於Parallel Old垃圾收集器的標記整理演算法進行垃圾回收。
CMS
CMS(Concurrent Mark Sweep)垃圾收集器是為老年代設計的垃圾收集器,其主要目的是達到最短的垃圾回收停頓時間,基於執行緒的標記清除演算法實現,以便在多執行緒併發環境下以最短的垃圾收集停頓時間提高系統的穩定性。
CMS的工作機制相對複雜,垃圾回收過程包含如下4個步驟。
- 初始標記:只標記和GC Roots直接關聯的物件,速度很快,需要暫停所有工作執行緒。
- 併發標記:和使用者執行緒一起工作,執行GC Roots跟蹤標記過程,不需要暫停工作執行緒。
- 重新標記:在併發標記過程中使用者執行緒繼續執行,導致在垃圾回收過程中部分物件的狀態發生變化,為了確保這部分物件的狀態正確性,需要對其重新標記並暫停工作執行緒。
- 併發清除:和使用者執行緒一起工作,執行清除GC Roots不可達物件的任務,不需要暫停工作執行緒。
CMS垃圾收集器在和使用者執行緒一起工作時(併發標記和併發清除)不需要暫停使用者執行緒,有效縮短了垃圾回收時系統的停頓時間,同時由於CMS垃圾收集器和使用者執行緒一起工作,因此其並行度和效率也有很大提升。
G1
G1(Garbage First)垃圾收集器為了避免全區域垃圾收集引起的系統停頓,將堆記憶體劃分為大小固定的幾個獨立區域,獨立使用這些區域的記憶體資源並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先順序列表,在垃圾回收過程中根據系統允許的最長垃圾收集時間,優先回收垃圾最多的區域。
G1垃圾收集器通過記憶體區域獨立劃分使用和根據不同優先順序回收各區域垃圾的機制,確保了G1垃圾收集器在有限時間內獲得最高的垃圾收集效率。
相對於CMS收集器,G1垃圾收集器兩個突出的改進。
- 基於標記整理演算法,不產生記憶體碎片。
- 可以精確地控制停頓時間,在不犧牲吞吐量的前提下實現短停頓垃圾回收。
類載入機制
JVM的類載入階段
JVM的類載入分為5個階段:載入、驗證、準備、解析、初始化。
在類初始化完成後就可以使用該類的資訊,在一個類不再被需要時可以從JVM中解除安裝。
載入
載入:載入是指JVM讀取 Class檔案,並且根據 Class檔案描述建立 java.lang.Class物件的過程。
類載入過程主要包含將 Class檔案讀取到執行時區域的方法區內,在堆中建立 java.lang.Class物件,並封裝類在方法區的資料結構的過程,在讀取 Class檔案時既可以通過檔案的形式讀取,也可以通過 JAR包、 WAR包讀取,還可以通過代理自動生成 Class或其他方式讀取。
驗證
驗證:驗證主要用於確保 Class檔案符合當前虛擬機器的要求,保障虛擬機器自身的安全,只有通過驗證的 Class檔案才能被JVM載入。
準備
準備:準備主要工作是在方法區中為類變數分配記憶體空間並設定類中變數的初始值。
初始值指不同資料型別的預設值,這裡需要注意 final型別的變數和非 final型別的變數在準備階段的資料初始化過程不同。
慄如,一個成員變數的定義如下:
public static long value = 1000;
在以上程式碼中,靜態變數value在準備階段的初始值是0,將value設定為1000的動作是在物件初始化時完成的,因為JVM在編譯階段會將靜態變數的初始化操作定義在構造器中。但是,如果將變數value宣告為final型別:
public static final int value = 1000;
則JVM在編譯階段後會為final型別的變數value生成其對應的ConstantValue屬性,虛擬機器在準備階段會根據ConstantValue屬性將value賦值為1000。
解析
解析:解析是指JVM會將常量池中的符號引用替換為直接引用。
初始化
初始化:初始化主要通過執行類構造器的 <client>方法為類進行初始化。
<client>方法是在編譯階段由編譯器自動收集類中靜態語句塊和變數的賦值操作組成的。
JVM規定,只有在父類的 <client>方法都執行成功後,子類中的 <client>方法才可以被執行。
在一個類中既沒有靜態變數賦值操作也沒有靜態語句塊時,編譯器不會為該類生成 <client>方法。
在發生以下幾種情況時,JVM不會執行類的初始化流程。
- 常量在編譯時會將其常量值存入使用該常量的類的常量池中,該過程不需要呼叫常量所在的類,因此不會觸發該常量類的初始化。
- 在子類引用父類的靜態欄位時,不會觸發子類的初始化,只會觸發父類的初始化。
- 定義物件陣列,不會觸發該類的初始化。
- 在使用類名獲取 Class物件時不會觸發類的初始化。
- 在使用 Class.forName載入指定的類時,可以通過 initialize引數設定是否需要對類進行初始化。
- 在使用 ClassLoader預設的 loadClass方法載入類時不會觸發該類的初始化。
類載入器
JVM提供了3種類載入器,分別是啟動類載入器、擴充套件類載入器和應用程式類載入器。
類載入器分為:啟動類載入器、擴充套件類載入器、應用程式類載入器、自定義載入器。
- 啟動類載入器:負責載入 Java_HOME/lib目錄中的類庫,或通過 -Xbootclasspath引數指定路徑中被虛擬機器認可的類庫。
- 擴充套件類載入器:負責載入 Java_HOME/lib/ext目錄中的類庫,或通過 java.ext.dirs系統變數載入指定路徑中的類庫。
- 應用程式類載入器:負責載入使用者路徑( classpath)上的類庫。
- 除了上述3種類載入器,我們也可以通過繼承 java.lang.ClassLoader實現自定義的類載入器。
雙親委派機制
JVM通過雙親委派機制對類進行載入。
雙親委派機制指一個類在收到類載入請求後不會嘗試自己載入這個類,而是把該類載入請求向上委派給其父類去完成,其父類在接收到該類載入請求後又會將其委派給自己的父類,以此類推,這樣所有的類載入請求都被向上委派到啟動類載入器中。
若父類載入器在接收到類載入請求後發現自己也無法載入該類(通常原因是該類的 Class檔案在父類的類載入路徑中不存在),則父類會將該資訊反饋給子類並向下委派子類載入器載入該類,直到該類被成功載入,若找不到該類,則JVM會丟擲 ClassNotFoud異常。
雙親委派類載入機制的類載入流程如下:
- 將自定義載入器掛載到應用程式類載入器。
- 應用程式類載入器將類載入請求委託給擴充套件類載入器。
- 擴充套件類載入器將類載入請求委託給啟動類載入器。
- 啟動類載入器在載入路徑下查詢並載入 Class檔案,如果未找到目標 Class檔案,則交由擴充套件類載入器載入。
- 擴充套件類載入器在載入路徑下查詢並載入 Class檔案,如果未找到目標 Class檔案,則交由應用程式類載入器載入。
- 應用程式類載入器在載入路徑下查詢並載入 Class檔案,如果未找到目標 Class檔案,則交由自定義載入器載入。
- 在自定義載入器下查詢並載入使用者指定目錄下的 Class檔案,如果在自定義載入路徑下未找到目標 Class檔案,則丟擲 ClassNotFoud異常。
雙親委派機制的核心是保障類的唯一性和安全性。
例如在載入 rt.jar包中的 java.lang.Object類時,無論是哪個類載入器載入這個類,最終都將類載入請求委託給啟動類載入器載入,這樣就保證了類載入的唯一性。
如果在JVM中存在包名和類名相同的兩個類,則該類將無法被載入,JVM也無法完成類載入流程。
OSGI
OSGI(Open Service Gateway Initiative)是Java動態化模組化系統的一系列規範,旨在為實現Java程式的模組化程式設計提供基礎條件。
基於OSGI的程式可以實現模組級的熱插拔功能,在程序升級更新時,可以只針對需要更新的程式進行停用和重新安裝,極大提高了系統升級的安全性和便捷性。
OSGI提供了一種面向服務的架構,該架構為元件提供了動態發現其他元件的功能,這樣無論是加入元件還是解除安裝元件,都能被系統的其他元件感知,以便各個元件之間能更好地協調工作。
OSGI不但定義了模組化開發的規範,還定義了實現這些規範所依賴的服務與架構,市場上也有成熟的框架對其進行實現和應用,但只有部分應用適合採用OSGI方式,因為它為了實現動態模組,不再遵循JVM類載入雙親委派機制和其他JVM規範,在安全性上有所犧牲。
推薦閱讀: