Java虛擬機(JVM, Java Virtual Machine)是運行Java應用程序的核心組件,它是一個抽象化的計算機系統模型,為Java字節碼提供運行環境。JVM的主要功能包括:類加載機制、內存管理、垃圾回收、指令解釋與執行、異常處理與安全檢查、性能優化等。
目錄
一、初識JVM
1.1、JVM基礎知識
1.2、JVM的組成部分
二、字節碼文件詳解
2.1、字節碼文件的基本組成
2.2、類加載器
三、JVM的內存區域
四、JVM的垃圾回收機制
4.1、自動垃圾回收
4.2、方法區的回收
4.3、堆內存的回收
一、初識JVM
1.1、JVM基礎知識
1.什么是JVM?
JVM是java虛擬機,本質上是運行在計算機上的程序,它的職責是運行Java字節碼文件。
正常我們編寫的是.java文件,通過javac進行編譯成.class的字節碼文件,然后由JVM進行加載并解釋成為機器碼。
2.JVM的三大功能?
JVM的第一個作用是類加載并對對字節碼文件的指令進行解釋成機器碼。
JVM的第二個功能是內存管理,主要包括為對象分配內存,以及垃圾回收等。
JVM的第三個功能是即時編譯,對熱點代碼進行優化,提升執行效率。
java語言是先編譯成字節碼文件,再通過jvm解釋成可執行的機器碼文件。C/C++是直接編譯鏈接成可執行的.exe文件,所以一般來說C/C++語言的性能更高。
Java語言的實時解釋主要是為了實現跨平臺特性,相當于一次編譯成字節碼文件,可以不同的平臺通過JVM進行解釋運行。
3.常見的JVM有哪些?
常見的虛擬機有如下幾種,其中HotSpot是JDK默認自帶的虛擬機。
1.2、JVM的組成部分
JVM主要包括如下幾部分:類加載器、運行時數據區、執行引擎、本地接口四個部分。
1.類加載器:加載class字節碼文件中的內容到內存中。
2.運行時數據區:負責管理JVM所使用的內存,比如創建對象和銷毀對象。
3.執行引擎:將字節碼中的文件解釋成機器碼,同時使用即時編譯器優化性能。
4.本地接口:調用本地已經編譯的方法,比如虛擬機中提供的c/c++方法。
其中運行時數據區包括大部分:
- 堆:存儲對象實例和數組,是所有線程共享的一塊區域,也是垃圾回收的主要區域。
- 虛擬機棧:每個線程都有自己的棧空間,用于存放基本類型的變量、對象引用以及方法調用的上下文信息(如局部變量表、操作數棧等)。
- 方法區:存儲類元數據(如類的結構信息)、靜態變量、常量池和即時編譯后的代碼等。
- 程序計數器:記錄當前線程執行字節碼的位置。
- 本地方法棧:為JNI調用的本地方法服務。
二、字節碼文件詳解
2.1、字節碼文件的基本組成
注意:直接使用記事本打開字節碼文件會是亂碼,一般來說使用jclasslib工具可以查看字節碼文件
字節碼文件主要包含如下5個部分:
1.基礎信息:魔數、字節碼對應的Java版本號、訪問標識、父類與接口。
2.常量池:保存了字符串常量、類或者接口名主要在字節碼指令中使用。
3.字段:當前類或者接口聲明的字段信息。
4.方法:當前類或者接口聲明的方法信息。
5.屬性:類的屬性,比如源碼的文件名、內部類的列表等。
2.2、類加載器
1.類的生命周期?
加載->連接(驗證、準備、解析)->初始化->使用->卸載
加載:類加載器根據類的全限名通過不同的渠道以二進制流的方式獲取字節碼信息。類加載完成后JVM會把字節碼信息放到方法區中。Java虛擬機還會在堆中生成一份與方法區中類似的java.lang.Class。
連接:驗證階段是驗證內容是否滿足java虛擬機規范,準備階段是靜態變量賦初值,解析階段是將常量池中的符號引用替換成指向內存的直接引用。
驗證:
1.對文件 格式的校驗以及主次版本號是否滿足當前Java虛擬機版本要求。例如:主版本號不允許超過運行環境的版本號,如果主版本號相等,副版本號也不能超過。
2.元信息驗證,比如類必須有父類。
3.驗證程序執行指令的語義。
4.符號引用驗證,例如是否訪問了其他類中private修飾的方法。
準備:
final修飾的基本數據類型的靜態變量,準備階段會對變量賦終值。
解析:
將常量池中的符號引用替換為直接引用。
符號引用:在字節碼文件中使用編號來訪問常量池中的內容。
直接引用:不再使用編號,使用內存中的地址進行具體數據的訪問。
初始化:
初始化階段會執行靜態代碼塊中的代碼,并為靜態變量賦初值。本質上就是初始化階段會執行字節碼文件中cliniit部分的指令。
以下幾種方式會導致類的初始化:
1.訪問一個類的靜態變量或者靜態方法,注意final修飾的常量不會觸發初始化 。
2.調用Class.forName()
3.new一個該類的對象時
4.執行Main方法中的當前類
2.常見的類加載器有哪些?
類加載器是Java虛擬機提供給應用程序去實現獲取類或接口的字節碼數據的技術。
啟動類加載器(BootStrap ClassLoader):用于加載一些核心類,由HotSpot虛擬機提供,使用 C++編寫的類加載器。
擴展類加載器器:用于加載一些擴展類,由JDK提供,使用Java編寫的類加載器。
應用程序類加載器:用于加載應用第三方jar包里的類,由JDK提供,使用Java編寫的類加載器。
3.雙親委派機制的作用,什么是雙親委派機制?
作用:通過雙親委派機制避免惡意代碼替換JDK中的核心類庫,確保核心類庫的完整性和安全性,另外可以避免同一個類被多次加載。
雙親委派機制:類加載器在加載之前會向上查找其父類是否加載過,如果父類加載過則直接返回,父類能未加載過則判斷能否加載(是否在類加載器的加載目錄路徑中),能加載就加載,不能加載則由子類加載。
4.如何打破雙親委派機制?為什么要打破雙親委派機制?
打破雙親委派機制的三種方式:
1.自定義類加載器并重寫LoadClass方法,就可以把雙親委派機制的代碼清除
2.利用上下文類加載器加載類,例如:JDBC和 JNDI等
3.利用Osgi框架的類加載器
- 實現特殊的需求:在某些情況下,可能需要加載特殊的類或者自定義類加載邏輯,例如實現熱替換(HotSwapping)功能,或者實現隔離的類加載環境,如OSGi、Tomcat容器等,這些場景下需要自定義類加載器并改變類加載的順序和規則。
- 實現版本隔離或多重類加載:在某些框架或應用服務器中,為了支持多個版本的同一類庫共存,或者為了實現模塊化加載,需要打破雙親委派,讓每個模塊擁有獨立的類加載器來加載自己的類。
三、JVM的內存區域
內存溢出(OOM):程序在使用某一塊內存區域時,存放的數據占用內存大小超出了虛擬機所能提供的最大內存上限。
1.Java的內存區域?
程序計數器(線程不共享):每個線程通過程序計數器記錄當前要執行的字節碼指令的地址。在程序執行過程中,程序計數器會記錄下一行字節碼指令的地址,執行完當前指令后,java虛擬機的執行引擎會根據指令計數器執行下一行 指令。具體來說程序計數器有兩個作用:1.記錄指令執行的地址并控制線程執行指令。2.多線程執行指令,線程切換時候用來記錄線程執行到指令的地址。
程序計數器不會產生內存溢出,因為每個線程只是存儲一個固定長度的內存地址。
Java虛擬機棧(線程不共享):Java虛擬機棧隨著線程的創建而創建,隨著線程的銷毀而回收。java虛擬機棧存放的是棧幀,棧幀包含三部分:局部變量表、操作數棧、幀數據。
局部變量表:程序在運行過程中,存放執行方法的所有局部變量。局部變量表本質上是一個數組,數組中每個位置稱為一個槽,long和double占2個槽,其它類型占1個槽。局部變量保存的內容:實例方法的this,方法的參數,方法體中聲明的局部變量。為了節省空間,局部變量表的槽是可以復用的,一旦某個局部變量失效,當前的槽就可以被復用。
操作數棧:是虛擬機在執行指令的過程用來存放臨時數據的一塊區域。比如:當前指令的值壓入操作數棧中,后面的指令可以彈出并使用該值。在編譯時期就可以確定操作數棧的最大深度,從而在執行的時候正確分配內存大小。
幀數據:動態鏈接、方法出口、異常表的引用。動態鏈接用于保存符號引用到運行時常量池內存地址的一個映射關系。方法出口存放的是棧幀中下一個指令的執行地址。異常表存放的是代碼中異常的處理信息,包括異常捕獲的生效范圍以及發生異常后跳轉到字節碼指令的位置。
要修改 Java虛擬機棧的大小,可以使用虛擬機參數-Xss,例如:-Xss256k
虛擬機HotSpot對棧的最大值與最小值有要求,例如:windows下jdk8的運行虛擬機棧的范圍在180k-1024m之間。另外,局部變量過多或者操作數棧深度過大也可能影響棧內存的大小。
本地方法棧(線程不共享):和Java虛擬機棧類似,用于服務native方法。Java虛擬機棧存儲的是Java方法調用時的棧幀,而本地方法棧存儲的是native本地方法的棧幀。在HotSpot虛擬機中,Java虛擬機棧和本地方法棧使用同一塊內存空間。
堆(線程共享):創建出來的對象存在堆內存中,堆內存的大小是有上限的,當一直向堆中放入對象達到上限之后就會拋出OutOfMemory(OOM)的錯誤。要修改虛擬機堆的大小可以使用參數:
-Xmx值(max最大值)? -Xms值(初始的total) 建議將這兩個值設置為相同大小的,這樣程序在啟動的時候使用的總的內存就是最大的內存,無需向Java虛擬機再次申請,減少堆內存申請的開銷。
方法區(線程共享):主要存放三部分的信息:1.類的基本信息,2.運行時常量池:字節碼文件的常量池內容,3.字符串常量池內容。在類加載階段,方法區會存放類的基本信息。JDK7及之前的版本將方法區放在堆內存中的永久代中,堆的大小由虛擬機控制;JDK8及之后的版本將方法區放到了元空間中,元空間位于操作系統維護的直接內存中,默認只要不超過操作系統承受的上限,則可以一直分配。運行時常量池主要用于存儲編譯期生成的各種字面量和符號引用。字符串常量池主要用于存儲字符串字面量的對象引用地址的一個區域。
注意:String類型的字符串,如果是變量拼接,則轉換成StringBuilder,如果是常量拼接,則直接拼接。
直接內存:JDK8及其以后方法區就存在直接內存的元空間中。
四、JVM的垃圾回收機制
4.1、自動垃圾回收
內存泄漏:指的是不再使用的對象在系統中未被回收,內存泄漏的累積可能會導致內存溢出。
在C/C++這類語言中沒有自動垃圾回收機制,一個對象不再使用,需要手動釋放,否則會導致內存泄漏 。
Java為了簡化對象的釋放,引入了自動垃圾回收機制,通過垃圾回收器來對不再使用的對象進行回收,垃圾回收器主要是對堆上的內存進行回收。
4.2、方法區的回收
我們知道類的聲明周期包括:加載,連接(驗證,準備,解析),初始化,使用,卸載五個部分,其中最后一步的卸載就是方法區的回收。
判斷一個類是否可以被卸載需要同時滿足以下三個條件:
1.此類的所有實例對象都已經被回收,在堆中不存在該類的任何實例對象以及子類對象。
2.加載該類的類加載器已經被回收。
3.該類對應的java.lang.Class對象在任何地方都沒有被引用。
如果需要手動回收垃圾,需要使用 System.gc()方法
4.3、堆內存的回收
1.常見的垃圾回收算法?
引用計數法與可達性分析法
引用計數法:為每個對象維護一個引用計數器,當對象被應用則加1,取消引用則減1。該方法優點是十分簡單。缺點:存在循環引用的問題,A引用B且B也引用A,假如應用程序不再需要這兩個對象,但是 引用 計數器不為0,所以導致這兩個對象無法回收,會出現內存泄漏。
可達性分析法:Java使用的是可達性分析算法來判斷對象是否可以回收,可達性分析將對象分為兩類:垃圾回收的根對象(GC Root)與普通對象,對象與對象之間存在引用關系。如果從普通對象到根對象的引用鏈是不可達的,則說明該對象則是可回收的。
那么哪些對象可以被稱為是GCRoot對象呢,以下四類可以被稱為垃圾回收根對象:
1)線程Thread對象
2)系統類加載器加載的 java.lang.Class對象
3)監視器對象,用來保存同步鎖synchronized關鍵字持有的對象
4)本地方法調用時使用的全局對象
標記清除法:在可達性分析算法確定了哪些對象是可達(存活)之后,垃圾收集器將對那些不可達的對象進行標記操作。標記完成后,垃圾收集器會清除所有被標記為不可達的對象占用的內存空間。優點:實現簡單,第一階段進行標記,第二階段進行清除即可。缺點:內存碎片化,清除掉一些碎片的內存可能導致無法再分配空間,維護鏈表進行分配,分配速度慢。
標記復制法:當前存活的對象復制到另一塊空間,將當前的空間進行清理掉。優點:吞吐量高,復制后不會發生碎片化。缺點:內存空間的使用率低?。
標記壓縮法:也稱為標記整理算法,使用可達性分析標記存活的對象,將存活的對象移動到內存的一側,清理掉存活對象之前的內存空間。優點:內存使用率高,不會產生碎片化。缺點:整理算法的效率不高。
分代GC算法:分代垃圾回收將整個內存區域劃分成年輕代與老年代。
年輕代:該區域包含三個部分:伊甸園區Eden,幸存者區S0,幸存者區S1。
創建出來的對象首先會放入伊甸園區,如果Eden區滿了,就會觸發young GC,如果可以被回收,則回收,否則放入幸存者區,如果年輕代的對象一直沒回收,存活時間比較長的對象會被放入到老年代。當老年代不足的時候,且年輕代也滿了,無法放入新的對象就會觸發 full GC。
2.常見的垃圾回收器?
為什么分代GC算法要把堆內存空間 劃分成年輕代與老年代?
因為年輕代用于存放可以很快被回收的對象,比如:用戶訂單數據。老年代則存放長期存活的對象,比如 Spring的大部分Bean對象。在虛擬機的默認設置中,新生代的大小 要遠小于老年代的大小。總的來說,分代GC算法將堆分成新生代和老年代的主要原因可以分為如下三點:
1.可以通過調整年輕代和老年代的比例來適應不同類型的應用程序,提高內存的利用率和性能。
2.新生代和老年代選擇不同的垃圾回收算法,新生代一般選擇標記復制法,老年代一般選擇標記清除法與標記壓縮法,由程序員來選擇,靈活度較高。
3.分代的設計只允許回收新生代,如果能滿足對象分配的要求就不需要對整個堆進行回收,即不需要full GC。
常見的垃圾回收器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old
Serial是一種單線程串行的年輕代垃圾回收器,使用的是標記復制法,適用于單cpu進行處理垃圾回收,不適用于多cpu下的處理,多cpu下可能會使得其它用戶線程處于長時間的等待狀態。
SerialOld是Serial的老年代版本,采用單線程串行回收,可以與Serial垃圾回收器搭配使用。
ParNew是年輕代的垃圾回收器,本質上是堆Serial進行多cpu下的優化,使用多線程進行垃圾回收,與老年代垃圾回收器CMS進行搭配使用。
CMS垃圾回收器關注的是系統的暫停時間,允許用戶線程和垃圾回收線程在某些步驟中同時進行,減少了用戶線程的等待時間。采用標記清除法,垃圾回收停頓的時間短,用戶體驗好,但是存在內存碎片化如果老年代內存不足會退化成單線程SerialOld回收。CMS適用于請求數據量大,頻率高的場景,例如:訂單接口、商品接口等。
Parallel Scavenge垃圾回收器:是JDK8默認的年輕代垃圾回收器,多線程并行回收,關注的是系統吞吐量,具備自動調節堆內存大小的特點。吞吐量高,但是不能保證單次的停頓時間,適用于后臺任務,比如:大數據的處理、大文件的導出等。
Parallel Old垃圾回收器:是為Parallel Scavenge設計的老年代的垃圾回收器,利用多線程并發收集,一般來說,使用Parallel Old+Parallel Scavenge組合,不需要設置堆內存的最大值,垃圾回收器會根據最大暫停時間和吞吐量自動調整堆內存的大小。
當發生STW時,JVM會暫停所有非垃圾收集線程(即除GC線程之外的所有用戶線程),STW事件會導致應用程序暫時失去響應,對于實時性要求高的應用來說,STW時間過長可能會對系統性能造成顯著影響。
G1垃圾回收器:是年輕代和老年代的垃圾回收器,JDK9開始之后默認的垃圾回收器,G1垃圾回收器是結合了CMS垃圾回收器與Parallel Scavenge垃圾回收器的優點,具備以下三個優點:1.支持大的堆空間的回收,并有較高的吞吐量。2.支持多CPU并行垃圾回收。3.允許用戶設置最大暫停時間。
G1的整個堆會被劃分成多個大小相等的區域,區域不要求連續,具體分為:伊甸園區、幸存者區、老年代區。G1的垃圾回收有兩種方式:年輕代回收(young GC),回收伊甸園區和幸存者區不用的對象,年輕代不足,會觸發young GC,使用標記復制法進行垃圾清理。
當某個對象被復制多次,存活年齡達到閾值15,該對象將會被移入老年代,多次回收后,會出現很多的老年代區,此時總堆的占有率達到閾值,此時會采用標記復制法回收年輕代與老年代的對象。G1老年代的清理會選擇存活度最低的區域進行回收,可以保證回收效率最高。如果發現沒有足夠的區域存放轉移的對象,會觸發full GC,單線程執行標記整理算法,會導致用戶線程的暫停。
3.Java中四種引用方式?
強引用:通過可達性分析判斷,對象被引用則不是垃圾,若對象到GCRoot不可達,可以認為是垃圾,會被回收掉。當內存不足的時候,只有對象被引用,就不會被回收。
軟引用:當程序內存不足的時候,軟引用對象會被回收掉。SoftReference類來實現軟引用。軟引用對象回收后,內存仍然不足可能會報OOM。
弱引用:使用弱引用的對象,再使用完成后無論是否存在引用都會被回收掉。弱引用使用WeakReference類來實現,弱引用主要在ThreadLocal中使用。
虛引用:虛引用的作用是對象被垃圾回收器回收的時候可以收到相應的通知。