JVM原理速記複習Java虛擬機器總結思維導圖面試必備
良心製作,右鍵另存為儲存
喜歡可以點個贊哦
Java虛擬機器
一、執行時資料區域
執行緒私有
程式計數器
- 記錄正在執行的虛擬機器位元組碼指令的地址(如果正在執行的是Native方法則為空),是唯一一個沒有規定OOM(OutOfMemoryError)的區域。
Java虛擬機器棧
- 每個Java方法在執行的同時會建立一個棧楨用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。從方法呼叫直到執行完成的過程,對應著一個棧楨在Java虛擬機器棧中入棧和出棧的過程。(區域性變數包含基本資料型別、物件引用reference和returnAddress型別)
本地方法棧
- 本地方法棧與Java虛擬機器棧類似,它們之間的區別只不過是本地方法棧為Native方法服務。
執行緒公有
Java堆(GC區)(Java Head)
- 幾乎所有的物件例項都在這裡分配記憶體,是垃圾收集器管理的主要區域。分為新生代和老年代。對於新生代又分為Eden空間、From Survivor空間、To Survivor空間。
JDK1.7 方法區(永久代)
- 用於存放已被載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
對這塊區域進行垃圾回收的主要目的是對常量池的回收和對類的解除安裝,但是一般難以實現。
HotSpot虛擬機器把它當做永久代來進行垃圾回收。但很難確定永久代的大小,因為它受到很多因素的影響,並且每次Full GC之後永久代的大小都會改變,所以經常丟擲OOM異常。 執行時常量池
- 是方法區的一部分
Class檔案中的常量池(編譯器生成的字面量和符號引用)會在類載入後被放入這個區域。
允許動態生成,例如String類的intern()
- 是方法區的一部分
- 用於存放已被載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
JDK1.8 元空間
- 原本存在方法區(永久代)的資料,一部分移到了Java堆裡面,一部分移到了本地記憶體裡面(即元空間)。元空間儲存類的元資訊,靜態變數和常量池等放入堆中。
直接記憶體
- 在NIO中,會使用Native函式庫直接分配堆外記憶體。
二、HotSpot虛擬機器
物件的建立
- 當虛擬機器遇到一條new指令時
- 檢查引數能否在常量池中找到符號引用,並檢查這個符號引用代表的類是否已經被載入、解析和初始過,沒有的話先執行相應的類載入過程。
- 在類載入檢查通過之後,接下來虛擬機器將為新生物件分配記憶體。
- 記憶體分配完成之後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭)。
- 對物件頭進行必要的設定。
- 執行構造方法按照程式設計師的意願進行初始化。
物件的記憶體佈局
- 物件頭
- 第一部分用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標識、執行緒持有的鎖、偏向執行緒ID、偏向實現戳等。
- 第二部分是型別指標,即物件指向它的類元資料的指標(如果使用直接物件指標訪問),虛擬機器通過這個指標來確定這個物件是哪個類的例項。
- 如果物件是一個Java陣列的話,還需要第三部分記錄資料長度的資料。
- 例項資料
- 是物件真正儲存的有效資訊,也就是在程式碼中定義的各種型別的欄位內容。
- 對齊填充
- 不是必然存在的,僅僅起著佔位符的作用。
HotSpot需要物件的大小必須是8位元組的整數倍。
物件的訪問定位
控制代碼訪問
- 在Java堆中劃分出一塊記憶體作為控制代碼池。
Java棧上的物件引用reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了到物件例項資料的指標和到物件型別資料的指標。
物件例項資料在Java堆中,物件型別資料在方法區(永久代)中。
優點:在物件被移動時只會改變控制代碼中的例項資料指標,而物件引用本身不需要修改。
- 在Java堆中劃分出一塊記憶體作為控制代碼池。
直接指標訪問(HotSpot使用)
- Java棧上的物件引用reference中儲存的就是物件的直接地址。
在堆中的物件例項資料就需要包含到物件型別資料的指標。
優點:節省了一次指標定位的時間開銷,速度更快。
- Java棧上的物件引用reference中儲存的就是物件的直接地址。
三、垃圾收集
概述
- 垃圾收集主要是針對Java堆和方法區。
程式計數器、Java虛擬機器棧個本地方法棧三個區域屬於執行緒私有,執行緒或方法結束之後就會消失,因此不需要對這三個區域進行垃圾回收。
判斷物件是否可以被回收
第一次標記(緩刑)
引用計數演算法
- 給物件新增一個引用計數器,當物件增加一個引用時引用計數值++,引用失效時引用計數值--,引用計數值為0時物件可以被回收。
但是它難以解決物件之間的相互迴圈引用的情況,此時這個兩個物件引用計數值為1,但是永遠無法用到這兩個物件。
- 可達性分析演算法(Java使用)
- 以一系列GC Roots的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連是,則證明此物件不可用,可以被回收。
GC Roots物件包括
- 虛擬機器棧(棧楨中的本地變量表)中引用的物件。
- 方法區中共類靜態屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中JNI(即一般說的Native方法)引用的物件。
第二次標記
- 當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過。
如果物件在finalize方法中重新與引用鏈上的任何一個物件建立關聯則將不會被回收。 finalize()
- 任何一個物件的finalize()方法都只會被系統呼叫一次。
它的出現是一個妥協,執行代價高昂,不確定性大,無法保證各個物件的呼叫順序。
finalize()能做的所有工作使用try-finally或者其他方式都可以做的更好,完全可以忘記在這個函式的存在。
- 任何一個物件的finalize()方法都只會被系統呼叫一次。
- 當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過。
方法區的回收
- 在方法區進行垃圾回收的價效比一般比較低。
主要回收兩部分,廢棄常量和無用的類。
滿足無用的類三個判斷條件才僅僅代表可以進行回收,不是必然關係,可以使用-Xnoclassgc引數控制。
- 該類的所有例項都已經被回收,也就是Java堆中不存在該類的任何例項。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問到該類的方法。
引用型別
- 強引用
- 使用new一個新物件的方式來建立強引用。
只要強引用還存在,被引用的物件則永遠不會被回收。
- 軟引用
- 使用SoftReference類來實現軟引用。
用來描述一些還有用但是並非必須的物件,被引用的物件在將要發生記憶體溢位異常之前會被回收。
- 弱引用
- 使用WeakReference類來實現弱引用。
強度比軟引用更弱一些,被引用的物件在下一次垃圾收集時會被回收。
- 虛引用
- 使用PhantomReference類來實現虛引用。
最弱的引用關係,不會對被引用的物件生存時間構成影響,也無法通過虛引用來取得一個物件例項。
唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
垃圾收集演算法
- 標記 - 清除
- 首先標記出所有需要回收的物件,在標記完成後統一回收被標記的物件並取消標記。
不足:
- 效率問題,標記和清除兩個過程的效率都不高。
- 空間問題,標記清除之後會產生大量不連續的記憶體碎片,沒有連續記憶體容納較大物件而不得不提前觸發另一次垃圾收集。
- 標記 - 整理
- 和標記 - 清除演算法一樣,但標記之後讓所有存活物件都向一段移動,然後直接清理掉端邊界以外的記憶體。
解決了標記 - 清除演算法的空間問題,但需要移動大量物件,還是存在效率問題。
- 複製
- 將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用多的記憶體空間一次清理掉。
代價是將記憶體縮小為原來的一般,太高了。
現在商業虛擬機器都採用這種演算法用於新生代。
因為新生代中的物件98%都是朝生暮死,所以將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor空間。
當回收時,如果另外一塊Survivor空間沒有足夠的空間存放存活下來的物件時,這些物件將直接通過分配擔保機制進入老年代。
- 分代收集
- 一般把Java堆分為新生代和老年代。
在新生代中使用複製演算法,在老年代中使用標記 -清除 或者 標記 - 整理 演算法來進行回收。
HotSpot的演算法實現
列舉根節點(GC Roots)
- 目前主流Java虛擬機器使用的都是準確式GC。
GC停頓的時候,虛擬機器可以通過OopMap資料結構(對映表)知道,在物件內的什麼偏移量上是什麼型別的資料,而且特定的位置記錄著棧和暫存器中哪些位置是引用。因此可以快速且準確的完成GC Roots列舉。
- 目前主流Java虛擬機器使用的都是準確式GC。
安全點
- 為了節省GC的空間成本,並不會為每條指令都生成OopMap,只是在“特定的位置”記錄OopMap,這些位置稱為安全點。
程式執行只有到達安全點時才能暫停,到達安全點有兩種方案。
- 搶斷式中斷(幾乎不使用)。GC時,先把所有執行緒中斷,如果有執行緒不在安全點,就恢復該執行緒,讓他跑到安全點。
- 主動式中斷(主要使用)。GC時,設定一個標誌,各個執行緒執行到安全點時輪詢這個標誌,發現標誌為直則掛起執行緒。
但是當執行緒sleep或blocked時無法響應JVM的中斷請求走到安全點中斷掛起,所以引出安全區域。
安全區域
- 安全區域是指在一段程式碼片段之中,引用關係不會發生變化,是擴充套件的安全點。
執行緒進入安全區域時表示自己進入了安全區域,這個發生GC時,JVM就不需要管這個執行緒。
執行緒離開安全區域時,檢查系統是否完成GC過程,沒有就等待可以離開安全區域的訊號為止,否者繼續執行。
垃圾收集器
新生代
- serial收集器
- 它是單執行緒收集器,只會使用一個執行緒進行垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作執行緒。
優點:對比其他單執行緒收集器簡單高效,對於單個CPU環境來說,沒有執行緒互動的開銷,因此擁有最高的單執行緒收集效率。
它是Client場景下預設新生代收集器,因為在該場景下記憶體一般來說不會很大。
- 2. parnew收集器
- 它是Serial收集器的多執行緒版本,公用了相當多的程式碼。
在單CPU環境中絕對不會有比Serial收集器更好的效果,甚至在2個CPU環境中也不能百分之百超越。
它是Server場景下預設的新生代收集器,主要因為除了Serial收集器,只用它能與CMS收集器配合使用。
- 3. parallel scavenge收集器
- “吞吐優先”收集器,與ParNew收集器差不多。
但是其他收集器的目標是儘可能縮短垃圾收集時使用者執行緒停頓的時間,而它的目標是達到一個可控制的吞吐量。這裡的吞吐量指CPU用於執行使用者程式的時間佔總時間的比值。
老年代
- serial old收集器
- 是Serial收集器老年代版本。
也是給Client場景下的虛擬機器使用的。
- 5. parallel old收集器
- 是Parallel Scavenge收集器的老年代版本。
在注重吞吐量已經CPU資源敏感的場合,都可以優先考慮Parallel Scavenge和Parallel Old收集器。
- 6. cms收集器
- Concurrent Mark Sweep收集器是一種以獲取最短回收停頓時間為目標的收集器。
- 運作過程
- 1. 初始標記(最短)。仍需要暫停使用者執行緒。只是標記一下GC Roots能直接關聯到的物件,速度很快
- 併發標記(耗時最長)。進行GC Roots Tracing(根搜尋演算法)的過程。
- 重新標記。修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄。比初始標記長但遠小於併發標記時間。
- 併發清除
1 和4 兩個步驟並沒有帶上併發兩個字,即這兩個步驟仍要暫停使用者執行緒。
- 優缺點
- 併發收集、低停頓。
- CMS收集器對CPU資源非常敏感。雖然不會導致使用者執行緒停頓,但是佔用CPU資源會使應用程式變慢。
- 無法處理浮動垃圾。在併發清除階段新垃圾還會不斷的產生,所以GC時要控制“-XX:CMSinitiatingOccupancyFraction引數”預留足夠的記憶體空間給這些垃圾,當預留記憶體無法滿足程式需要時就會出現”Concurrent Mode Failure“失敗,臨時啟動Serial Old收集。
- 由於使用標記 - 清除演算法,收集之後會產生大量空間碎片。
- g1收集器
- Garbage First是一款面向服務端應用的垃圾收集器
運作過程
- 初始標記
- 併發標記
- 最終標記
- 刪選標記
五、類載入機制
概述
- 虛擬機器把描述類的資料從Class問價載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。
Java應用程式的高度靈活性就是依賴執行期動態載入和動態連線實現的。
類的生命週期
- 載入 -> 連線(驗證 -> 準備 -> 解析) -> 初始化 -> 使用 - >解除安裝
類初始化時機
主動引用
- 虛擬機器規範中沒有強制約束何時進行載入,但是規定了有且只有五種情況必須對類進行初始化(載入、驗證、準備都會隨之發生)
- 遇到new、getstatic、putstatic、invokestatic這四條位元組碼指令時沒有初始化。
- 反射呼叫時沒有初始化。
- 發現其父類沒有初始化則先觸發其父類的初始化。
- 包含psvm(mian()方法)的那個類。
- 動態語言支援時,REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼。
被動引用
- 除上面五種情況之外,所有引用類的方式都不會觸發初始化,稱為被動引用。
- 通過子類引用父類的靜態欄位,不會導致子類的初始化。
- 通過陣列定義來引用類,不會觸發此類的初始化。該過程會對陣列類進行初始化,陣列類是一個由虛擬機器自動生成的、直接繼承Object的子類,其中包含陣列的屬性和方法,使用者只能使用public的length和clone()。
- 常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
類載入過程
- 載入
- 通過類的全限定名來獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.Class物件(HotSpot將其存放在方法區中),作為方法區這個類的各種資料的訪問入口。
- 驗證
- 為了確保Class檔案的位元組類中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。可以通過-Xverify:none關閉大部分類驗證。
- 檔案格式驗證。確保輸入位元組流能正確的解析並存儲於方法區,後面的3個驗證全部基於方法區的儲存結構進行,不會再操作位元組流。
- 元資料驗證。對位元組碼描述資訊進行語義分析,確保其符合Java語法規範。(Java語法驗證)
- 位元組碼驗證。最複雜,通過資料流和控制流分析,確定程式語義時合法的、符合邏輯的。可以通過引數關閉。(驗證指令跳轉範圍,型別轉換有效等)
- 符號引用驗證。將符號引用轉化為直接引用,發生在第三個階段——解析階段中發生。
- 準備
- 類變數是被static修飾的變數,準備階段為類變數分配記憶體並設定零值(final直接設定初始值),使用的是方法區的記憶體。
- 解析
- 將常量池內的符號引用替換為直接引用的過程。
其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支援Java的動態繫結。
解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼、和呼叫點限定符。
- 初始化
初始化階段才真正執行類中定義的Java程式程式碼,是執行類構造器
()方法的過程。
在準備階段,類變數已經給過零值,而在初始化階段,根據程式設計師通過程式制定的主觀計劃去初始化類變數和其他資源。() - 類構造器方法。是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的的語句合併產生的。
- 不需要顯式呼叫父類構造器,JVM會保證在子類clinit執行之前,父類的clinit已經執行完成。
- 介面中不能使用靜態語句塊但仍可以有類變數的賦值操作。當沒有使用父介面中定義的變數時子介面的clinit不需要先執行父介面的clinit方法。介面的實現類也不會執行介面的clinit方法。
虛擬機器會保證clinit在多執行緒環境中被正確的加鎖、同步。其他線性喚醒之後不會再進入clinit方法,同一個類載入器下,一個型別只會初始化一次。
- <init>() - 物件構造器方法。Java物件被建立時才會進行例項化操作,對非靜態變數解析初始化。
會顯式的呼叫父類的init方法,物件例項化過程中對例項域的初始化操作全部在init方法中進行。
類(載入) 器
類與類載入器
- 類載入器實現類的載入動作。
類載入器和這個類本身一同確立這個類的唯一性,每個類載入器都有獨立的類名稱空間。在同一個類載入器載入的情況下才會有兩個類相等。
相等包括類的Class物件的equals()方法、isAssignableFrom()方法、isInstance()、instanceof關鍵字。
- 類載入器實現類的載入動作。
類載入器分類
啟動類載入器
- 由C++語言實現,是虛擬機器的一部分。負責將JAVA_HOME/lib目錄中,或者被-Xbootclasspath引數指定的路徑,但是檔名要能被虛擬機器識別,名字不符合無法被啟動類載入器載入。啟動類載入器無法被Java程式直接引用。
擴充套件類載入器
- 由Java語言實現,負責載入JAVA_HOME/lib/ext目錄,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。
應用程式類載入器
- 由於這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱他為系統類載入器。負責載入使用者類路徑(ClassPath)上所指定的類庫,一般情況下這個就是程式中預設的類載入器。
自定義類載入器
- 由使用者自己實現。
- 如果不想打破雙親委派模型,那麼只需要重寫findClass方法即可。
- 否則就重寫整個loadClass方法。
雙親委派模型
- 雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器。父子不會以繼承的關係類實現,而是都是使用組合關係來服用父載入器的程式碼。
在java.lang.ClassLoader的loadClass()方法中實現。 工作過程
- 一個類載入器首先將類載入請求轉發到父類載入器,只有當父類載入器無法完成(它的搜尋範圍中沒有找到所需要的類)時才嘗試自己載入
好處
- Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,從而使得基礎類庫得到同意。
- 雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應該有自己的父類載入器。父子不會以繼承的關係類實現,而是都是使用組合關係來服用父載入器的程式碼。
四、記憶體分配與回收策略
Minor GC 和 Full GC
Minor GC
- 發生在新生代的垃圾收集動作,因為新生代物件存活時間很短,因此Minor GC會頻繁執行,執行速度快。
時機
- Eden不足
Full GC
- 發生在老年區的GC,出現Full GC時往往伴隨著Minor GC,比Minor GC慢10倍以上。
時機
- 呼叫System.gc()
- 只是建議虛擬機器執行Full GC,但是虛擬機器不一定真正去執行。
不建議使用這種方式,而是讓虛擬機器管理記憶體。
- 老年代空間不足
- 常見場景就是大物件和長期存活物件進入老年代。
儘量避免建立過大的物件以及陣列,調大新生代大小,讓物件儘量咋新生代中被回收,不進入老年代。
- JDK1.7 之前方法區空間不足
- 當系統中要載入的類、反射的類和常量較多時,永久代可能會被佔滿,在未配置CMS GC的情況下也會執行Full GC,如果空間仍然不夠則會丟擲OOM異常。
可採用增大方法區空間或轉為使用CMS GC。
- 空間分配擔保失敗
- 發生Minor GC時分配擔保的兩個判斷失敗
- Concurrent Mode Failure
- CMS GC 併發清理階段使用者執行緒還在執行,不斷有新的浮動垃圾產生,當預留空間不足時報Concurrent Mode Failure錯誤並觸發Full GC。
記憶體分配策略
- 物件優先在Eden分配
- 大多數情況下,物件在新生代Eden上分配,當Eden空間不夠時,發起Minor GC,當另外一個Survivor空間不足時則將存活物件通過分配擔保機制提前轉移到老年代。
- 大物件直接進入老年代
- 配置引數-XX:PretenureSizeThreshold,大於此值得物件直接在老年代分配,避免在Eden和Survivor之間的大量記憶體複製。
- 長期存活物件進入老年代
- 虛擬機器為每個物件定義了一個Age計數器,物件在Eden出生並經過Minor GC存活轉移到另一個Survivor空間中時Age++,增加到預設16則轉移到老年代。
- 動態物件年齡繫結
- 虛擬機器並不是永遠要求物件的年齡必須到達MaxTenuringThreshold才能晉升老年代,如果在Survivor中相同年齡所有物件大小總和大於Survivor空間的一半,則年齡大於或等於該年齡的物件直接進入老年代。
- 空間分配擔保
- 在發生Minor GC之前,虛擬機器先檢查老年代最大可用的連續空間是否大於新生代的所有物件,如果條件成立,那麼Minor GC可以認為是安全的。
可以通過HandlePromotionFailure引數設定允許冒險,此時虛擬機器將與歷代晉升到老年區物件的平均大小比較,仍小於則要進行一次Full GC。
在JDK1.6.24之後HandlePromotionFailure已無作用,即虛擬機器預設為true。