Java 程序員必備基礎圖
前言
最近看了深入理解 Java 虛擬機第三版,整理了一些基礎結構圖,算是比較全的了,做一下筆記,大家一起學習。
1.Java 虛擬機運行時數據區圖
JVM 內存結構是 Java 程序員必須掌握的基礎。
程序計數器
-
程序計數器,可以看作當前線程所執行的字節碼的行號指示器
-
它是線程私有的。
Java 虛擬機棧
-
線程私有的,生命週期與線程相同。
-
每個方法被執行的時候都會創建一個 "棧幀", 用於存儲局部變量表 (包括參數)、操作數棧、動態鏈接、方法出口等信息。
-
局部變量表存放各種基本數據類型 boolean、byte、char、short 等
本地方法棧
- 與虛擬機棧基本類似,區別在於虛擬機棧爲虛擬機執行的 java 方法服務,而本地方法棧則是爲 Native 方法服務。
Java 堆
-
Java 堆是 java 虛擬機所管理的內存中最大的一塊內存區域,也是被各個線程共享的內存區域,在 JVM 啓動時創建。
-
其大小通過 - Xms 和 - Xmx 參數設置,-Xms 爲 JVM 啓動時申請的最小內存,-Xmx 爲 JVM 可申請的最大內存。
方法區
- 它用於存儲虛擬機加載的類信息、常量、靜態變量、是各個線程共享的內存區域。- 可以通過 - XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
2. 堆的默認分配圖
-
Java 堆 = 老年代 + 新生代
-
新生代 = Eden + S0 + S1
-
新生代與老年代默認比例的值爲 1:2 ,可以通過參數 –XX:NewRatio 配置。
-
默認的,Eden : from : to = 8 : 1 : 1 ,可以通過參數–XX:SurvivorRatio 來設定
3. 方法區結構圖
方法區是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。
4. 對象的內存佈局圖
一個 Java 對象在堆內存中包括對象頭、實例數據和補齊填充 3 個部分:
-
對象頭包括 Mark Word(存儲哈希碼,GC 分代年齡等) 和 類型指針(對象指向它的類型元數據的指針),如果是數組對象,還有一個保存數組長度的空間
-
實例數據是對象真正存儲的有效信息,包括了對象的所有成員變量,其大小由各個成員變量的大小共同決定。
-
對齊填充不是必然存在的,僅僅起佔位符的作用。
5. 對象頭的 Mark Word 圖
-
Mark Word 用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等。
-
在 32 位的 HotSpot 虛擬機中,如果對象處於未被鎖定的狀態下,那麼 Mark Word 的 32bit 空間裏的 25 位用於存儲對象哈希碼,4bit 用於存儲對象分代年齡,2bit 用於存儲鎖標誌位,1bit 固定爲 0,表示非偏向鎖。
6. 對象與 Monitor 關聯結構圖
對象是如何跟 monitor 有關聯的呢?
一個 Java 對象在堆內存中包括對象頭,對象頭有 Mark word,Mark word 存儲着鎖狀態,鎖指針指向 monitor 地址。這其實是 Synchronized 的底層哦~
7.Java Monitor 的工作機理圖:
Java 線程同步底層就是監視鎖 Monitor~,如下是 Java Monitor 的工作機理圖:
-
想要獲取 monitor 的線程, 首先會進入_EntryList 隊列。
-
當某個線程獲取到對象的 monitor 後, 進入_Owner 區域,設置爲當前線程, 同時計數器_count 加 1。
-
如果線程調用了 wait() 方法,則會進入_WaitSet 隊列。它會釋放 monitor 鎖,即將_owner 賦值爲 null,_count 自減 1, 進入_WaitSet 隊列阻塞等待。
-
如果其他線程調用 notify() / notifyAll() ,會喚醒_WaitSet 中的某個線程,該線程再次嘗試獲取 monitor 鎖,成功即進入_Owner 區域。
-
同步方法執行完畢了,線程退出臨界區,會將 monitor 的 owner 設爲 null,並釋放監視鎖。。
8. 創建一個對象內存分配流程圖
-
對象一般是在 Eden 區生成。
-
如果 Eden 區填滿,就會觸發 Young GC。
-
觸發 Young GC 的時候,Eden 區實現清除,沒有被引用的對象直接被清除。
-
依然存活的對象,會被送到 Survivor 區,Survivor =S0+S1.
-
每次 Young GC 時,存活的對象複製到未使用的那塊 Survivor 區,當前正在使用的另外一塊 Survivor 區完全清除,接着交換兩塊 Survivor 區的使用狀態。
-
如果 Young GC 要移送的對象大於 Survivor 區上限,對象直接進入老年代。
-
一個對象不可能一直呆在新生代,如果它經過多次 GC,依然活着,次數超過 - XX:MaxTenuringThreshold 的閥值,它直接進入老年代。簡言之就是,對象經歷多次滾滾長江,紅塵世事,終於成爲長者(進入老年代)
9. 可達性分析算法判定對象存活
可達性分析算法是用來判斷一個對象是否存活的~
算法的核心思想:
- 通過一系列稱爲 “GC Roots” 的對象作爲起始點,從這些節點開始根據引用關係向下搜索,搜索走過的路徑稱爲 “引用鏈”,當一個對象到 GC Roots 沒有任何的引用鏈相連時(從 GC Roots 到這個對象不可達) 時,證明此對象不可能再被使用。
10. 標記 - 清除算法示意圖
-
標記 - 清除算法是最基礎的垃圾收集算法。
-
算法分爲兩個階段,標記和清除。
-
首先標記出需要回收的對象,標記完成後,統一回收掉被標記的對象。
-
當然可以反過來,先標記存活的對象,統一回收未被標記的對象。
-
標記 - 清除 兩個缺點是,執行效率不穩定和內存空間的碎片化問題~
11. 標記 - 複製算法示意圖
-
1969 年 Fenichel 提出 “半區複製”,將內存容量劃分對等兩塊,每次只使用一塊。當這一塊內存用完,將還存活的對象複製到另外一塊,然後把已使用過的內存空間一次清理掉~
-
1989 年,Andrew Appel 提出 “Appel 式回收”,把新生代劃分爲較大的 Eden 和兩塊較小的 Survivor 空間。每次分配內存只使用 Eden 和其中一塊 Survivor 空間。發生垃圾收集時,將 Eden 和 Survivor 中仍然存活的對象一次性複製到另外一塊 Survivor 空間上。Eden 和 Survivor 比例是 8:1~
-
“半區複製” 缺點是浪費可用空間,並且,如果對象存活率高的話,複製次數就會變多,效率也會降低。
12. 標記 - 整理算法示意圖
-
1974 年,Edward 提出 “標記 - 整理” 算法,標記過程跟 “標記 - 清除” 算法一樣,接着讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存~
-
標記 - 清除算法和標記整理算法本質差異是:前者是一種非移動式的回收算法,後者是移動式的回收算法。
-
是否移動存活對象都存在優缺點,移動雖然內存回收複雜,但是從程序吞吐量來看,更划算;不移動時內存分配更復雜,但是垃圾收集的停頓時間會更短,所以看收集器取捨問題~
-
Parallel Scavenge 收集器是基於標記 - 整理算法的,因爲關注吞吐。CMS 收集器是基於標記 - 清除算法的,因爲它關注的是延遲。
13. 垃圾收集器組合圖
-
新生代收集器:Serial、ParNew、Parallel Scavenge
-
老年代收集器:CMS、Serial Old、Parallel Old
-
混合收集器:G1
14. 類的生命週期圖
一個類從被加載到虛擬機內存開始,到卸載出內存爲止,這個生命週期經歷了七個階段:加載、驗證、準備、解析、初始化、使用、卸載。
加載階段:
-
通過一個類的全限定名來獲取定義此類的二進制字節流。
-
將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
-
在內存中生成一個代表這個類的 java.lang.Class 對象,作爲方法區這個類的各種數據的訪問入口
驗證:
-
驗證的目的是確保 Class 文件的字節流中包含的信息滿足約束要求,保證這些代碼運行時不會危害虛擬機自身安全
-
驗證階段有:文件格式校驗、元數據校驗、字節碼校驗、符號引用校驗。
準備
- 準備階段是正式爲類中定義的變量(靜態變量)分配內存並設置類變量初始值的階段。
解析
- 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
初始化
- 到了初始化階段,才真正開始執行類中定義的 Java 字節碼。
15. 類加載器雙親委派模型圖
雙親委派模型構成
啓動類加載器,擴展類加載器,應用程序類加載器,自定義類加載器
雙親委派模型工作過程是
如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器完成。每個類加載器都是如此,只有當父加載器在自己的搜索範圍內找不到指定的類時(即 ClassNotFoundException),子加載器纔會嘗試自己去加載。
爲什麼需要雙親委派模型?
如果沒有雙親委派,那麼用戶是不是可以自己定義一個 java.lang.Object 的同名類,java.lang.String 的同名類,並把它放到 ClassPath 中, 那麼類之間的比較結果及類的唯一性將無法保證,因此,雙親委派模型可以防止內存中出現多份同樣的字節碼。
16. 棧幀概念結構圖
棧幀是用於支持虛擬機進行方法調用和方法執行背後的數據結構。棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址信息。
局部變量表
-
是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。
-
局部變量表的容量以變量槽 (Variable Slot) 爲最小單位。
操作數棧
-
操作數棧,也稱操作棧,是一個後入先出棧。
-
當一個方法剛剛開始執行的時候, 該方法的操作數棧也是空的, 在方法的執行過程中, 會有各種字節碼指令往操作數棧中寫入和提取內容, 也就是出棧與入棧操作。
動態連接
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用, 持有引用是爲了支持方法調用過程中的動態連接 (Dynamic Linking)。
方法返回地址
- 當一個方法開始執行時, 只有兩種方式退出這個方法 。一種是執行引擎遇到任意一個方法返回的字節碼指令。另外一種退出方式是在方法執行過程中遇到了異常。
17.Java 內存模型圖
-
Java 內存模型規定了所有的變量都存儲在主內存中
-
每條線程還有自己的工作內存
-
線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝
-
線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。
-
不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。
18. 線程狀態轉換關係圖
Java 語言定義了 6 種線程池狀態:
-
新建(New):創建後尚未啓動的線程處於這種狀態
-
運行(Running):線程開啓
start()
方法,會進入該狀態。 -
無限等待(Waiting):處於這種狀態的線程不會被分配處理器執行時間,一般
LockSupport::park()
, 沒有設置了 Timeoout 的Object::wait()
方法,會讓線程陷入無限等待狀態。 -
限期等待(Timed Waiting):處於這種狀態的線程不會被分配處理器執行時間,在一定時間之後他們會由系統自動喚醒。
sleep()
方法會進入該狀態~ -
阻塞(Blocked):在程序等待進入同步區域的時候,線程將進入這種狀態~
-
結束(Terminated):已終止線程的線程狀態,線程已經結束執行
19. Class 文件格式圖
-
u1、u2、u4、u8 分別代表 1 個字節、2 個字節、4 個字節和 8 個字節的無符號數
-
表是由多個無符號數或者其他表作爲數據項構成的複合數據類型
-
每個 Class 文件的頭四個字節被稱爲魔數(記得以前校招面試,面試官問過我什麼叫魔數。。。)
-
minor 和 major version 表示次版本號,主版本號
-
緊接着主次版本號之後,是常量池入口,常量池可以比喻爲 Class 文件裏的資源倉庫~
20.JVM 參數思維導圖
JVM 調優是通往高級開發的必經橋樑,所以好好積累 JVM 參數配置哈~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2Y-49p473OhZZpo3bllHGQ