1. JVM的體系結(jié)構(gòu)
類加載器(ClassLoader)
作用:負(fù)責(zé)加載字節(jié)碼文件(.class文件)到內(nèi)存中。它是JVM執(zhí)行類加載機(jī)制的基礎(chǔ)組件,將類的字節(jié)碼數(shù)據(jù)加載到*區(qū),在堆中創(chuàng)建對應(yīng)的Class對象作為*區(qū)中類數(shù)據(jù)的訪問入口。
分類:主要包括啟動類加載器(Bootstrap ClassLoader),它負(fù)責(zé)加載Java核心類庫(如java.lang包中的類),是由C++實(shí)現(xiàn)的,是JVM的一部分;擴(kuò)展類加載器(Ex* ClassLoader),用于加載Java的擴(kuò)展庫(位于jre/lib/ext目錄下);應(yīng)用程序類加載器(Application ClassLoader),也稱為系統(tǒng)類加載器,負(fù)責(zé)加載用戶類路徑(classpath)下的類。
雙親委派模型:這是類加載器的一種工作機(jī)制。當(dāng)一個類加載器收到類加載請求時,它首先會把請求委派給父類加載器。只有當(dāng)父類加載器無法完成該加載任務(wù)時(它的搜索范圍中沒有找到所需的類),子加載器才會嘗試自己加載。這種模型可以避免類的重復(fù)加載,并且保證了Java核心類庫的安全性,例如,用戶自定義的java.lang.Object類不會被加載,因?yàn)閱宇惣虞d器已經(jīng)加載了系統(tǒng)的java.lang.Object類。
運(yùn)行時數(shù)據(jù)區(qū)(Runtime Data Areas)
程序計(jì)數(shù)器(Program Counter Register):它是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器工作時就是通過改變這個計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計(jì)數(shù)器來完成。它是線程私有的,每個線程都有自己獨(dú)立的程序計(jì)數(shù)器,這樣可以保證各個線程按自己的執(zhí)行順序執(zhí)行字節(jié)碼。
Java虛擬機(jī)棧(Java Virtual Machine Stacks):它也是線程私有的,生命周期與線程相同。虛擬機(jī)棧描述的是Java*執(zhí)行的內(nèi)存模型,每個*在執(zhí)行時都會創(chuàng)建一個棧幀(Stack Frame),用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、*出口等信息。當(dāng)一個*被調(diào)用時,一個新的棧幀就會被壓入棧中;當(dāng)*執(zhí)行完成后,棧幀就會從棧中彈出。如果棧的深度超過了虛擬機(jī)允許的范圍,就會拋出StackOverflowError異常;如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展,但是在擴(kuò)展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
本地*棧(Native Method Stacks):與Java虛擬機(jī)棧類似,不過它是為本地(Native)*服務(wù)的。本地*是指用非Java語言(如C或C++)編寫的,并且被Java代碼調(diào)用的*。它的具體實(shí)現(xiàn)方式和內(nèi)存分配方式可能因JVM的不同而有所差異,在某些JVM實(shí)現(xiàn)中,本地*棧和Java虛擬機(jī)棧是合二為一的。同樣,本地*棧也會出現(xiàn)StackOverflowError和OutOfMemoryError異常。
堆(Heap):它是JVM管理的內(nèi)存中*的一塊,是被所有線程共享的一塊內(nèi)存區(qū)域。幾乎所有的對象實(shí)例和數(shù)組都在堆上分配內(nèi)存。堆的內(nèi)存空間是不連續(xù)的,它主要分為新生代(Young Generation)和老年代(Old Generation)。新生代又可以細(xì)分為Eden空間、From Survivor空間和To Survivor空間。垃圾收集器主要就是針對堆內(nèi)存進(jìn)行回收操作,以釋放那些不再被引用的對象所占用的空間。因?yàn)槎咽枪蚕淼模⑶倚枰l繁地進(jìn)行對象的創(chuàng)建和銷毀,所以它也是最容易出現(xiàn)OutOfMemoryError異常的區(qū)域。
*區(qū)(Method Area):它也是所有線程共享的內(nèi)存區(qū)域,用于存儲已被虛擬機(jī)加載的類信息(包括類的版本、字段、*、接口等信息)、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。在Java 8之前,*區(qū)是通過*代(PermGen)實(shí)現(xiàn)的,*代有固定的大小限制,容易出現(xiàn)OutOfMemoryError異常。在Java 8及以后,*區(qū)被元空間(Met*ace)取代,元空間使用本地內(nèi)存,理論上它的大小只受限于本地內(nèi)存的大小,不過也需要合理配置參數(shù),否則也可能出現(xiàn)內(nèi)存問題。
2. 垃圾回收(Garbage Collection,GC)
垃圾回收的基本原理
引用計(jì)數(shù)法(Reference Counting):這是一種簡單的垃圾回收算法。每個對象都有一個引用計(jì)數(shù)器,當(dāng)有一個地方引用這個對象時,計(jì)數(shù)器就加1;當(dāng)引用失效時,計(jì)數(shù)器就減1。當(dāng)計(jì)數(shù)器的值為0時,就表示這個對象可以被回收了。但是這種*無法解決循環(huán)引用的問題,例如,對象A引用對象B,對象B又引用對象A,此時它們的引用計(jì)數(shù)都不為0,但實(shí)際上這兩個對象可能已經(jīng)沒有其他有效的外部引用了,應(yīng)該被回收。
可達(dá)性分析算法(Reachability *ysis):這是目前主流JVM使用的垃圾回收算法。它以一系列被稱為“GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)。當(dāng)一個對象到GC Roots沒有任何引用鏈相連(即不可達(dá))時,則證明此對象是可以被回收的。GC Roots對象包括虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象、本地*棧中JNI(Java Native Inte*ce)引用的對象、*區(qū)中類靜態(tài)屬性引用的對象、*區(qū)中常量引用的對象等。
垃圾收集器(Garbage Collector)
Serial收集器:這是最基本、歷史最悠久的收集器。它是一個單線程收集器,在進(jìn)行垃圾收集時,必須暫停其他所有的工作線程,直到收集結(jié)束。它的優(yōu)點(diǎn)是簡單高效,對于限定單個CPU的環(huán)境來說,由于沒有線程交互的開銷,專心做垃圾收集可以獲得*的單線程收集效率。
ParNew收集器:它是Serial收集器的多線程版本。除了使用多線程進(jìn)行垃圾收集外,其余行為包括收集算法、Stop
The
World機(jī)制等都和Serial收集器一樣。它是許多運(yùn)行在Server模式下的JVM虛擬機(jī)*的新生代收集器,因?yàn)樗芘cCMS收集器(老年代收集器)很好地配合工作。
Parallel Scavenge收集器:它也是一個新生代收集器,采用復(fù)制算法。它的特點(diǎn)是關(guān)注的是吞吐量(Throughput),即CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值。它提供了兩個參數(shù)用于*控制吞吐量,如
XX:MaxGCPauseMillis(控制*垃圾收集停頓時間)和
XX:GCTimeRatio(直接設(shè)置吞吐量大小)。
CMS收集器(Concurrent Mark Sweep):這是一種以獲取最短回收停頓時間為目標(biāo)的老年代收集器。它的工作過程比較復(fù)雜,主要分為四個階段:初始標(biāo)記(Initial Mark)、并發(fā)標(biāo)記(Concurrent Mark)、重新標(biāo)記(Re
Mark)和并發(fā)清除(Concurrent Sweep)。其中初始標(biāo)記和重新標(biāo)記這兩個階段需要暫停所有用戶線程(Stop
The
World),但時間比較短;并發(fā)標(biāo)記和并發(fā)清除階段是與用戶線程同時進(jìn)行的,這樣就可以在一定程度上減少垃圾收集時對用戶線程的影響,從而提高應(yīng)用程序的響應(yīng)速度。不過,CMS收集器也有一些缺點(diǎn),比如它對CPU資源比較敏感,在并發(fā)階段會占用一部分CPU資源,導(dǎo)致應(yīng)用程序的性能下降;而且它會產(chǎn)生大量的空間碎片,需要定期進(jìn)行碎片整理。
Garbage
First(G1)收集器:它是一款面向服務(wù)端應(yīng)用的垃圾收集器,主要應(yīng)用于多處理器和大容量內(nèi)存環(huán)境。G1收集器在收集過程中不會產(chǎn)生空間碎片,它把堆內(nèi)存劃分成多個大小相等的獨(dú)立區(qū)域(Region),在進(jìn)行垃圾回收時,會優(yōu)先回收垃圾最多的區(qū)域。它采用了標(biāo)記
整理(Mark
Compact)和復(fù)制(Copy)算法相結(jié)合的方式。G1收集器可以*地控制停頓時間,通過設(shè)置
XX:MaxGCPauseMillis參數(shù)來指定目標(biāo)停頓時間,它會盡量在這個時間范圍內(nèi)完成垃圾收集工作。 ### 3. JVM性能調(diào)優(yōu)
性能指標(biāo)
響應(yīng)時間(Resp*e Time):指從用戶發(fā)出請求到收到響應(yīng)的時間間隔。在JVM性能調(diào)優(yōu)中,需要關(guān)注*執(zhí)行時間、線程阻塞時間等因素對響應(yīng)時間的影響。例如,一個Web應(yīng)用程序,用戶點(diǎn)擊一個按鈕后,等待服務(wù)器返回?cái)?shù)據(jù)的時間就是響應(yīng)時間。如果響應(yīng)時間過長,用戶體驗(yàn)就會很差。
吞吐量(Throughput):是指單位時間內(nèi)系統(tǒng)處理的請求數(shù)量。對于一個處理大量并發(fā)請求的服務(wù)器來說,吞吐量是一個重要的性能指標(biāo)。例如,一個每秒能夠處理100個HTTP請求的Web服務(wù)器,其吞吐量就是100個請求/秒。在調(diào)優(yōu)過程中,需要平衡吞吐量和響應(yīng)時間之間的關(guān)系。
內(nèi)存占用(Memory Footprint):指JVM進(jìn)程占用的內(nèi)存大小。包括堆內(nèi)存、棧內(nèi)存、*區(qū)內(nèi)存等各個部分的占用情況。如果內(nèi)存占用過高,可能會導(dǎo)致系統(tǒng)頻繁地進(jìn)行垃圾回收,甚至出現(xiàn)OutOfMemoryError異常。例如,一個Java應(yīng)用程序在處理大量數(shù)據(jù)時,需要合理配置堆內(nèi)存大小,以避免內(nèi)存溢出。
調(diào)優(yōu)工具
JDK自帶的工具
jc*ole:它是一個基于JMX(Java Management Extensi*)的可視化監(jiān)控工具,可以用來監(jiān)控Java應(yīng)用程序的運(yùn)行時狀態(tài),包括內(nèi)存使用情況、線程狀態(tài)、類加載情況等。通過jc*ole,可以直觀地看到堆內(nèi)存的使用量、各個線程的狀態(tài)(如運(yùn)行、阻塞、等待等),并且可以檢測到死鎖等問題。
jvisualvm:它是一個功能更強(qiáng)大的多合一工具,不僅可以監(jiān)控Java應(yīng)用程序的性能,還可以進(jìn)行性能分析和故障排查。它可以生成詳細(xì)的性能報告,包括*的執(zhí)行時間、對象的分配情況等。例如,可以通過jvisualvm來分析一個應(yīng)用程序中哪個*占用了大量的時間,從而對其進(jìn)行優(yōu)化。
第三方工具
YourKit Java Profiler:這是一款商業(yè)的Java性能分析工具,它提供了非常詳細(xì)的性能分析功能,包括CPU使用率分析、內(nèi)存泄漏檢測、線程性能分析等。它可以幫助開發(fā)人員深入了解應(yīng)用程序的性能瓶頸,并且提供了多種可視化的圖表來展示分析結(jié)果。
調(diào)優(yōu)策略
調(diào)整堆內(nèi)存大?。焊鶕?jù)應(yīng)用程序的實(shí)際需求,合理配置堆內(nèi)存的大小。如果應(yīng)用程序需要處理大量的對象,并且內(nèi)存占用比較高,可以適當(dāng)增加堆內(nèi)存的大小。但是,過大的堆內(nèi)存也可能會導(dǎo)致垃圾回收時間過長。例如,對于一個內(nèi)存密集型的應(yīng)用程序,可以通過設(shè)置
Xmx(*堆內(nèi)存)和
Xms(初始堆內(nèi)存)參數(shù)來調(diào)整堆內(nèi)存大小。
選擇合適的垃圾收集器:根據(jù)應(yīng)用程序的性能要求和特點(diǎn),選擇合適的垃圾收集器。例如,如果應(yīng)用程序?qū)憫?yīng)時間比較敏感,要求盡量減少垃圾收集時的停頓時間,可以選擇CMS收集器或者G1收集器;如果應(yīng)用程序?qū)ν掏铝恳蟊容^高,對停頓時間不是特別敏感,可以選擇Parallel Scavenge收集器。
優(yōu)化代碼層面:在代碼層面進(jìn)行優(yōu)化也是提高JVM性能的重要手段。例如,盡量減少對象的創(chuàng)建和銷毀,避免在循環(huán)中創(chuàng)建大量的臨時對象;合理使用緩存,減少重復(fù)計(jì)算;及時釋放資源,避免資源泄漏等。 ### 4. JVM字節(jié)碼和指令集
字節(jié)碼(Bytecode)
概念:Java源代碼經(jīng)過編譯器編譯后生成的中間形式的代碼就是字節(jié)碼。字節(jié)碼是一種二進(jìn)制格式的代碼,它不依賴于具體的硬件平臺和操作系統(tǒng),具有良好的可移植性。字節(jié)碼文件(.class文件)的結(jié)構(gòu)是按照J(rèn)VM規(guī)范定義的,它包含了類的各種信息,如常量池、類的訪問標(biāo)志、字段和*的信息等。
示例:以一個簡單的Java類為例,如`public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }`,這個類經(jīng)過編譯后會生成一個字節(jié)碼文件。通過反編譯工具(如javap)可以查看字節(jié)碼的內(nèi)容,字節(jié)碼中包含了很多指令,如`ldc`(將常量池中的常量加載到操作數(shù)棧)、`invokevirtual`(調(diào)用實(shí)例*)等,這些指令是JVM執(zhí)行的最小單位。
指令集(Instruction Set)
概念:JVM指令集是JVM能夠識別和執(zhí)行的一套指令規(guī)范。它包括操作碼(Opcode)和操作數(shù)(Operand)兩部分。操作碼用于指定要執(zhí)行的操作類型,如加載、存儲、運(yùn)算、跳轉(zhuǎn)等;操作數(shù)則是操作的對象或者數(shù)據(jù)。不同的JVM實(shí)現(xiàn)可能會對指令集有一些細(xì)微的差異,但都必須遵循JVM規(guī)范。
示例:在JVM指令集中,`aload_0`指令用于將*個引用類型本地變量加載到操作數(shù)棧頂。如果在一個*中有一個本地變量是一個對象引用,就可以使用這個指令將其加載到操作數(shù)棧,以便后續(xù)進(jìn)行*調(diào)用或者其他操作。