本文是基於jdk8進行分析的
概述
JVM體系結構
Java虛擬機包含類裝載器子系統、執行引擎、運行時數據區、本地方法接口和垃圾收集模塊。其中垃圾收集模塊在Java虛擬機規范中並沒有要求Java虛擬機垃圾收集,但是在沒有發明無限的內存之前,大多數JVM實現都是有垃圾收集的。
- 類裝載器子系統:根據給定的全限定類名(如:java.lang.Object)來裝載class文件到運行時數據區域的方法區中。
- 執行引擎:執行字節碼或執行本地方法。
- 運行時數據區:我們常說的JVM的內存,堆,方法區,虛擬機棧,本地方法棧,程序計數器。
- 本地方法接口:與本地方法庫交互,作用就是為了融合不同編程語言為Java所用,它的初衷是融合C/C++程序。
首先通過編譯器把Java代碼轉換成字節碼,類加載器再把字節碼加載到內存中(運行時數據區的方法區內),而字節碼文件只是JVM的一套指令集規范,不能直接交給底層系統去執行,所以需要特定的命令解析器執行引擎將字節碼翻譯成底層系統指令,再交給CPI去執行,而這個過程需要調用其他語言的本地庫接口來實現整個程序的功能。
類加載機制
Java類加載機制就是虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的java類型。
類加載器
類加載器分為啟動類加載器,擴展類加載器,應用程序類加載器,自定義類加載器。各種類加載器之間存在着邏輯上的父子關系,但不是真正意義上的父子關系,因為它們直接沒有從屬關系。除了啟動類加載器(Bootstrap ClassLoader)是由C++編寫的,其他都是由Java編寫的。由Java編寫的類加載器都繼承自類java.lang.ClassLoader。
- 啟動類加載器(BootstrapClassLoader):負責加載$JAVA_HOME/jre/lib目錄下的核心類庫,比如rt.jar,charsets.jar。
- 擴展類加載器(ExtClassLoader):負責加載支撐J$JAVA_HOME/jre/lib/ext目錄下的JAR類包。父加載器是啟動類加載器。
- 應用類加載器(AppClassLoader):負責加載ClassPath路徑下的類包,主要就是加載我們自己寫的那些類。父加載器是擴展類加載器。
- 自定義類加載器(CustomClassLoader):負責加載用戶自定義目錄下的類包。父加載器是應用類加載器。
類加載過程
java類加載分為5個過程,加載-->驗證-->准備-->解析-->初始化。這5個階段一般是順序發生的,但在動態綁定的情況下,解析階段發生在初始化階段之后。
- 加載:將字節碼從不同的數據源(可能是 class 文件,也可能是 jar 包,甚至網絡)轉化為二進制字節流加載到內存中;將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;並在堆中生成一個代表該類的
java.lang.Class
對象,作為對方法區這個類的各種數據的訪問入口。 - 驗證:驗證的目的是為了確保加載進來的class文件符合JVM的規范,一般是進行文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
- 文件格式的驗證:驗證字節流是否符合Class文件格式的規范,並且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區之內。經過該階段的驗證后,字節流才會進入內存的方法區中進行存儲,后面的三個驗證都是基於方法區的存儲結構進行的。
- 元數據的驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合Java語法規范的元數據信息。
- 字節碼驗證:該階段驗證的主要工作是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會做出危害虛擬機安全的行為。
- 符號引用驗證:這是最后一個階段的驗證,它發生在虛擬機將符號引用轉化為直接引用的時候(解析階段中發生該轉化),主要是對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。
- 准備:給類的靜態變量分配空間,並賦予默認值。
- 解析:將符號引用替換為直接引用,該階段會把一些靜態方法(符號引用,比如main()方法)替換為指向數據所存內存的指針或句柄等(直接引用),這是所謂的靜態鏈接過程(類加載期間完成),動態鏈接是在程序運行期間完成的將符號引用替換為直接引用。
- 初始化:對類的靜態變量初始化為指定的值,執行靜態代碼塊。
雙親委派機制
雙親委派機制就是當某個類加載器收到加載類的請求,如果這個類沒有被加載過,該類加載器不會直接加載,會先為委派給父加載器,如果父加載器沒有加載過,依次往上傳遞,直到頂層啟動類加載器。如果父加載器可以完成加載任務,則父加載器加載返回;如果父加載器不能完成加載任務,才會自己去進行加載。一句話概述雙親委派機制加載流程就是,從下往上檢查類是否已經被加載,從上往下嘗試去加載。
雙親委派機制的優點:
- 沙箱安全機制:避免核心API被篡改。自己寫的java.lang.String.class類不會被加載。
- 避免重復加載:如果父加載器已經加載過該類,子類加載器就沒有必要再去加載。
雙親委派機制加載類的核心代碼 ClassLoader類的loadClass()方法:
1 protected Class<?> loadClass(String name, boolean resolve) 2 throws ClassNotFoundException 3 { 4 synchronized (getClassLoadingLock(name)) { 5 // 首先會檢查該類是否已經被本類加載器加載,如果已經被加載則直接返回
6 Class<?> c = findLoadedClass(name); 7 if (c == null) { 8 // 如果沒有被加載,則委托父加載器去加載
9 long t0 = System.nanoTime(); 10 try { 11 if (parent != null) { 12 // 讓父加載器對象去調用loadClass方法
13 c = parent.loadClass(name, false); 14 } else { 15 // parent==null,說明父加載器是啟動類加載器。啟動類加載器是C++編寫的,這里去調用本地方法區嘗試加載該類。
16 c = findBootstrapClassOrNull(name); 17 } 18 } catch (ClassNotFoundException e) { 19 } 20 if (c == null) { 21 // If still not found, then invoke findClass in order 22 // to find the class.
23 long t1 = System.nanoTime(); 24 // 如果父加載器沒有加載到該類,則自己去加載。這里會調用URLClassLoader類的findClass()方法
25 c = findClass(name); 26 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 27 sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 28 sun.misc.PerfCounter.getFindClasses().increment(); 29 } 30 } 31 if (resolve) { 32 resolveClass(c); 33 } 34 return c; 35 } 36 }
全盤負責委托機制
全盤負責委托機制就是當一個Classloader加載一個Class的時候,這個Class所依賴的和引用的其它Class通常也由這個Classloader負責加載。
打破雙親委派機制
打破雙親委派機制就是我們希望自定義類加載器去直接加載指定類,而不是先委托父加載器去加載或者是自定義類加載器加載不到才讓父加載器去進行加載。
自定義類加載器實現
了解雙親委派機制以及打破雙親委派機制之后,我們可以自己寫一個自定義類加載器。自定義類加載器實現思路:
如果使用雙親委派機制就是重寫findClass()方法(類加載器具體去加載類的方法),代碼傳送門。
如果要打破雙親委派機制,在重寫findClass()方法基礎上,還需要重新loadClass()方法,這里我們可以改寫邏輯,先讓該類加載器去加載類,加載不到再讓父加載器去進行加載,代碼傳送門。
JVM運行時數據區
-
程序計數器
程序計數器線程私有的,它的生命周期與線程相同,它是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果線程當前正在執行的方法是本地方法,這個計數器值則應為空。字節碼解釋器的工作就是通過這個計數器的值,來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能,都需要依賴這個計數器來完成。這個區域是唯一不會拋出OutOfMemoryError異常的區域。
-
虛擬機棧
虛擬機棧是線程私有的,它的生命周期與線程相同。每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯期可知的各種Java虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double),對象引用和returnAddress類型。
以下異常條件與Java虛擬機棧相關:
如果線程中請求的棧深度大於虛擬機所允許的深度,Java虛擬機將會拋出StackOverflowError異常。
如果Java虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存,Java虛擬機將會拋出OutOfMemoryError異常。
-
本地方法棧
本地方法棧是線程私有的,生命周期與當前線程一致。與虛擬機棧的作用是一樣的,區別只是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧是為虛擬機使用到的本地方法服務。
以下異常條件與本地方法棧相關聯(與虛擬機棧一樣):
如果線程中請求的棧深度大於虛擬機所允許的深度,Java虛擬機將會拋出StackOverflowError異常。
如果Java虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存,Java虛擬機將會拋出OutOfMemoryError異常。
-
堆
堆是線程共享的,在虛擬機啟動的時候創建,從中分配類實例(幾乎所有的對象都存放在堆中,但是不是所有的)和數組的內存。對於大多數應用來說,堆是內存最大的一塊區域;同時堆是內存模型中最重要的一個區域,也是JVM調優重點關注的區域。對象的堆存儲由自動存儲管理系統(稱為垃圾收集器)回收;對象永遠不會顯式釋放。
以下異常情況與堆相關聯:
如果在Java堆中沒有內存完成實例分配,並且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。
堆內存分為年輕代(Young Generation)和老年代(Old Generation)。
- 年輕代(YoungGen):年輕代又分為Eden和Survivor區。Survivor區由FromSpace和ToSpace組成。Eden區占大容量,Survivor兩個區占小容量,默認比例是8:1:1。
- 老年代(OldGen)。
-
方法區(元空間)
方法區是線程共享的,在虛擬機啟動的時候創建,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
以下異常條件與方法區域相關聯:
如果方法區無法滿足新的內存分配需求時,Java虛擬機將會拋出OutOfMemoryError異常。
-
運行時常量池
運行時常量池是方法區的一部分。它包含多種常量,范圍從編譯時已知的數字文字到必須在運行時解析的方法和字段引用。運行時常量池的功能類似於常規編程語言的符號表,盡管它包含的數據范圍比典型的符號表還大。每個運行時常量池都是從Java虛擬機的方法區分配的。當Java虛擬機創建類或接口時,將為該類或接口構造運行時常量池。
以下異常條件與類或接口的運行時常量池的構造相關聯:
如果運行時常量池無法再申請到內存時,則Java虛擬機將拋出OutOfMemoryError異常
。
-
直接內存
直接內存並不是虛擬機運行時數據區的一部分,也不是《Java虛擬機規范》中定義的內存區域。但是這部分內存有時候會使用,而且也可能導致OutOfMemoryError異常出現,所以這里簡單提一下。直接內存的分配不會受到Java 堆大小的限制,既然是內存,肯定還是會受到本機總內存的大小及處理器尋址空間的限制。服務器管理員配置虛擬機參數時,一般會根據實際內存設置-Xmx等參數信息,但經常會忽略掉直接內存,使得各個內存區域的總和大於物理內存限制從而導致動態擴展時出現OutOfMemoryError異常。
垃圾回收機制
在java中,程序員是不需要顯示的去釋放一個對象的內存的,而是由虛擬機自行執行。在JVM中,有一個垃圾回收線程,它是低優先級的,在正常情況下是不會執行的,只有在虛擬機空閑或者當前堆內存不足時,才會觸發執行,掃描那些沒有被任何引用的對象,並將它們添加到要回收的集合中,進行回收。
GC對象判定方法
-
引用計數法:為每個對象創建一個引用計數器,有對象引用時計數器+1,引用被釋放時計數器-1,當計數器為0時就可以被回收。它有一個缺點就是不能解決循環引用的問題。
- 可達性算法(引用鏈法):從GC Roots 開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots 沒有任何引用鏈相連時,則證明此對象是可以被回收的。
垃圾收集算法
-
分代收集理論
- 標記—清除算法
標記無用對象,然后進行清除回收。缺點:效率不高,無法清除垃圾碎片。
-
標記—復制算法
按照容量划分為2個大小相等的內存區域,當一塊用完的時候將活着的對象復制到另一塊上,然后再把已使用區域的內存空間一次清理掉。缺點:內存使用率不高,只有原來的一半。
- 標記—整理算法
標記無用對象,讓所有存活對象都向一端移動,然后直接清除掉端邊界以外的內存。
垃圾收集器
-
Serial 收集器(標記—復制算法):新生代單線程收集器,標記和清理都是單線程,優點是簡單高效。
-
ParNew 收集器(標記—復制算法):新生代並行收集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現。
-
Parallel Scavenge 收集器(標記—復制算法):新生代並行收集器,追求高吞吐量,高效利用CPU。吞吐量=用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量可以高效的利用CPU時間,盡快完成程序的運算任務,適合后台應用等對交互相應要求不高的場景。
-
Serial Old 收集器(標記—整理算法):老年代單線程收集器,Serial收集器的老年代版本。
-
Parallel Old 收集器(標記—整理算法):老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本。
-
Concurrent Mark Sweep(CMS)收集器(標記—清除算法):老年代並行收集器,以獲取最短回收停頓時間為目標的收集器,具有高並發、低停頓的特點,追求最短GC回收停頓時間。
-
Garbage First(G1)收集器(標記—整理算法):Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於“標記—整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的范圍僅限於新生代或者老年代。
JVM調優參數
- -Xms4g:初始化堆大小為4g
- -Xmx4g:堆最大內存為4g
- -XX:NewRatio=4:設置年輕代和老年代的內存比例為1:4
- -XX:SurvivorRatio=8:設置新生代Eden和Survivor比例為8:2(8:1:1)
- -XX:+UseParNewGC:指定使用ParNew + Serial Old 垃圾回收器組合
- -XX:+UseParallelOldGC:指定使用ParNew + ParNew Old 垃圾回收器組合
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器組合
- -XX:+PrintGC:開啟打印gc信息
- -XX:+PrintGCDetails:打印gc詳細信息