良心製作,JVM原理速記複習Java虛擬機總結思維導圖面試必備。 一、運行時數據區域 線程私有 程式計數器 記錄正在執行的虛擬機位元組碼指令的地址(如果正在執行的是Native方法則為空),是唯一一個沒有規定OOM(OutOfMemoryError)的區域。 Java虛擬機棧 每個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異常。
從JDK1.8開始,移除永久代,並把方法區移至元空間。 運行時常量池
- 是方法區的一部分
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。