JVM必备指南
簡(jiǎn)介
Java虛擬機(jī)(JVM)是Java應(yīng)用的運(yùn)行環(huán)境,從一般意義上來(lái)講,JVM是通過(guò)規(guī)范來(lái)定義的一個(gè)虛擬的計(jì)算機(jī),被設(shè)計(jì)用來(lái)解釋執(zhí)行從Java源碼編譯而來(lái)的字節(jié)碼。更通俗地說(shuō),JVM是指對(duì)這個(gè)規(guī)范的具體實(shí)現(xiàn)。這種實(shí)現(xiàn)基于嚴(yán)格的指令集和全面的內(nèi)存模型。另外,JVM也通常被形容為對(duì)軟件運(yùn)行時(shí)環(huán)境的實(shí)現(xiàn)。通常JVM實(shí)現(xiàn)主要指的是HotSpot。
JVM規(guī)范保證任何的實(shí)現(xiàn)都能夠以同樣的方式解釋執(zhí)行字節(jié)碼。其實(shí)現(xiàn)可以多樣化,包括進(jìn)程、獨(dú)立的Java操作系統(tǒng)或者直接執(zhí)行字節(jié)碼的處理器芯片。我們了解最多的JVM是作為軟件實(shí)現(xiàn),運(yùn)行在流行的操作系統(tǒng)平臺(tái)上(包括Windows、OS X、Linux和Solaris等)。
JVM的結(jié)構(gòu)允許對(duì)一個(gè)Java應(yīng)用進(jìn)行更細(xì)微的控制。這些應(yīng)用運(yùn)行在沙箱(Sandbox)環(huán)境中。確保在沒(méi)有恰當(dāng)?shù)脑S可時(shí),無(wú)法訪問(wèn)到本地文件系統(tǒng)、處理器和網(wǎng)絡(luò)連接。遠(yuǎn)程執(zhí)行時(shí),代碼還需要進(jìn)行證書(shū)認(rèn)證。
除了解釋執(zhí)行Java字節(jié)碼,大多數(shù)的JVM實(shí)現(xiàn)還包含一個(gè)JIT(just-in-time 即時(shí))編譯器,用于為常用的方法生成機(jī)器碼。機(jī)器碼使用的是CPU的本地語(yǔ)言,相比字節(jié)碼有著更快的運(yùn)行速度。
雖然理解JVM不是開(kāi)發(fā)或運(yùn)行Java程序的必要條件,但是如果多了解一些JVM知識(shí),那么就有機(jī)會(huì)避免很多性能上的問(wèn)題。理解了JVM,實(shí)際上這些問(wèn)題會(huì)變得簡(jiǎn)單明了。
體系結(jié)構(gòu)
JVM規(guī)范定義了一系列子系統(tǒng)以及它們的外部行為。JVM主要有以下子系統(tǒng):
- Class Loader 類加載器。 用于讀入Java源代碼并將類加載到數(shù)據(jù)區(qū)。
- Execution Engine 執(zhí)行引擎。 執(zhí)行來(lái)自數(shù)據(jù)區(qū)的指令。
數(shù)據(jù)區(qū)使用的是底層操作系統(tǒng)分配給JVM的內(nèi)存。
類加載器(Class Loader)
JVM在下面幾種不同的層面使用不同的類加載器:
- bootstrap class loader(引導(dǎo)類加載器):是其他類加載器的父類,它用于加載Java核心庫(kù),并且是唯一一個(gè)用本地代碼編寫的類加載器。
- extension class loader(擴(kuò)展類加載器):是bootstrap class loader加載器的子類,用于加載擴(kuò)展庫(kù)。
- system class loader(系統(tǒng)類加載器):是extension class loader加載器的子類,用于加載在classpath中的應(yīng)用程序的類文件。
- user-defined class loader(用戶定義的類加載器):是系統(tǒng)類加載器或其他用戶定義的類加載器的子類。
當(dāng)一個(gè)類加載器收到一個(gè)加載類的請(qǐng)求,首先它會(huì)檢查緩存,確認(rèn)該類是否已經(jīng)被加載,然后把請(qǐng)求代理給它的父類。如果父類沒(méi)能成功的加載類,那么子類就會(huì)自己去嘗試加載該類。子類可檢查父類加載器的緩存,但父類不能看到子類所加載的類。之所類加載體系會(huì)這樣設(shè)計(jì),是認(rèn)為一個(gè)子類不應(yīng)該重復(fù)加載已經(jīng)被父類加載過(guò)的類。
執(zhí)行引擎(Execution Engine)
執(zhí)行引擎一個(gè)接一個(gè)地執(zhí)行被加載到數(shù)據(jù)區(qū)的字節(jié)碼。為了保證字節(jié)碼指令對(duì)于機(jī)器來(lái)說(shuō)是可讀的,執(zhí)行引擎使用下面兩個(gè)方法:
- 解釋執(zhí)行:執(zhí)行引擎把它遇到的每一條指令解釋為機(jī)器語(yǔ)言。
- 即時(shí)編譯:如果一條指令經(jīng)常被使用,執(zhí)行引擎會(huì)把它編譯為本地代碼并存儲(chǔ)在緩存中。這樣,所有和這個(gè)方法相關(guān)的代碼都會(huì)直接執(zhí)行,從而避免重復(fù)解釋。
盡管即時(shí)編譯比解釋執(zhí)行要占用更多的時(shí)間,但是對(duì)于需要使用成千上萬(wàn)次的方法,只需要處理一次。相比每次都解釋執(zhí)行,以本地代碼的方式運(yùn)行會(huì)節(jié)約很多執(zhí)行時(shí)間。
JVM規(guī)范中并不規(guī)定一定要使用即時(shí)編譯。即時(shí)編譯也不是用于提高JVM性能的唯一的手段。規(guī)范僅僅規(guī)定了每條字節(jié)碼對(duì)應(yīng)的本地代碼,至于執(zhí)行引擎如何實(shí)現(xiàn)這一對(duì)應(yīng)過(guò)程的,完全由JVM的具體實(shí)現(xiàn)來(lái)決定。
內(nèi)存模型(Memory Model)
Java內(nèi)存模型建立在自動(dòng)內(nèi)存管理的概念之上。當(dāng)一個(gè)對(duì)象不再被一個(gè)應(yīng)用所引用,垃圾回收器就會(huì)回收它,從而釋放相應(yīng)的內(nèi)存。這一點(diǎn)和其他很多需要自行釋放內(nèi)存的語(yǔ)言有很大不同。
JVM從底層操作系統(tǒng)中分配內(nèi)存,并將它們分為以下幾個(gè)區(qū)域:
- 堆空間(Heap Space):這是共享的內(nèi)存區(qū)域,用于存儲(chǔ)可以被垃圾回收器回收的對(duì)象。
- 方法區(qū)(Method Area):這塊區(qū)域以前被稱作“永生代”(permanent generation),用于存儲(chǔ)被加載的類。這塊區(qū)域最近被JVM取消了。現(xiàn)在,被加載的類作為元數(shù)據(jù)加載到底層操作系統(tǒng)的本地內(nèi)存區(qū)。
- 本地區(qū)(Native Area):這個(gè)區(qū)域用于存儲(chǔ)基本類型的引用和變量。
一個(gè)有效的管理內(nèi)存方法是把對(duì)空間劃分為不同代,這樣垃圾回收器就不用掃描整個(gè)堆區(qū)。大多數(shù)的對(duì)象的生命周期都很段短暫,那些生命周期較長(zhǎng)的對(duì)象往往直到應(yīng)用退出才需要被清除。
當(dāng)一個(gè)Java應(yīng)用創(chuàng)建了一個(gè)對(duì)象,這個(gè)對(duì)象是被存儲(chǔ)到“初生池”(eden pool)。一旦初生池存儲(chǔ)滿了,就會(huì)在新生代觸發(fā)一次minor gc(小范圍的垃圾回收)。首先,垃圾回收器會(huì)標(biāo)記出那些“死對(duì)象”(不再被應(yīng)用所引用的對(duì)象),同時(shí)延長(zhǎng)所有保留對(duì)象的生命周期(這個(gè)生命周期長(zhǎng)度是用數(shù)字來(lái)描述,代表了期所經(jīng)歷過(guò)的垃圾回收的次數(shù))。然后,垃圾回收器會(huì)回收這些死對(duì)象,并把剩余的活著的對(duì)象移動(dòng)到“幸存池”(survivor pool),從而清空初生池。
當(dāng)一個(gè)對(duì)象存活達(dá)到一定的周期后,它就會(huì)被移動(dòng)到堆中的老生代:“終身代”(tenured pool)。最后,當(dāng)終身代被填滿時(shí),就會(huì)觸發(fā)一次full gc或major gc(完全的垃圾回收),以清理終身代。
(譯者注:一般我們把初生池和幸存池所在的區(qū)域合并成為新生代,把終身代所在的區(qū)域成為老生代。對(duì)應(yīng)的,在新生代上產(chǎn)生的gc稱為minor gc,在老生代上產(chǎn)生的gc稱為full gc。希望這樣大家在其他地方看到對(duì)應(yīng)的術(shù)語(yǔ)時(shí)能更好理解)
當(dāng)垃圾回收(gc)執(zhí)行的時(shí)候,所有應(yīng)用線程都要被停止,系統(tǒng)產(chǎn)生一次暫停。minor gc非常頻繁,所以被優(yōu)化的能夠快速的回收死對(duì)象,是新生代的內(nèi)存的主要的回收方式。major gc運(yùn)行起來(lái)就相對(duì)慢得多,因?yàn)橐獟呙璺浅6嗟幕钪膶?duì)象。垃圾回收器本身也有多種實(shí)現(xiàn),有些垃圾回收器在一定情況下能更快的執(zhí)行major gc。
堆的大小是動(dòng)態(tài)的,只有堆需要擴(kuò)張的時(shí)候才會(huì)從內(nèi)存中分配。當(dāng)堆被填滿時(shí),JVM會(huì)重新給堆分配更多的內(nèi)存,直到達(dá)到堆大小的上限,這種重新分配同樣會(huì)導(dǎo)致應(yīng)用的短暫停止。
線程
JVM是運(yùn)行在一個(gè)獨(dú)立的進(jìn)程中的,但它可以并發(fā)執(zhí)行多個(gè)線程,每個(gè)線程都運(yùn)行自己的方法,這是Java必備的一個(gè)部分。以即時(shí)消息客戶端這樣一個(gè)應(yīng)用為例,它至少運(yùn)行兩個(gè)線程。一個(gè)線程用于等待用戶輸入,另一個(gè)檢查服務(wù)端是否有新的消息傳輸。再以服務(wù)端應(yīng)用為例,有時(shí)一個(gè)請(qǐng)求可能要涉及多個(gè)線程并發(fā)執(zhí)行,所以需要多線程來(lái)處理請(qǐng)求。
在JVM的進(jìn)程中,所有的線程共享內(nèi)存和其他可用的資源。每一個(gè)JVM進(jìn)程在進(jìn)入點(diǎn)(main方法)處都要啟動(dòng)一個(gè)主線程,其他線程都從主線程啟動(dòng),成為執(zhí)行過(guò)程中的一個(gè)獨(dú)立部分。線程可以再不同的處理器上并行執(zhí)行,同樣也可以共享一個(gè)處理器,線程調(diào)度器負(fù)責(zé)處理多個(gè)線程共享一個(gè)處理器的情況。
很多應(yīng)用(特別是服務(wù)端應(yīng)用)會(huì)處理很多任務(wù),需要并行運(yùn)行。這些任務(wù)中有些是非常重要的,需要實(shí)時(shí)執(zhí)行的。而另外一些是后臺(tái)任務(wù),可以在CPU空閑時(shí)執(zhí)行。任務(wù)是在不同的線程中運(yùn)行的。舉例子來(lái)說(shuō),服務(wù)端可能有一些低優(yōu)先級(jí)的線程,它們會(huì)根據(jù)一些數(shù)據(jù)來(lái)計(jì)算統(tǒng)計(jì)信息。同時(shí)也會(huì)啟動(dòng)一些高優(yōu)先級(jí)的進(jìn)程用于處理傳入的數(shù)據(jù),響應(yīng)對(duì)這些統(tǒng)計(jì)信息的請(qǐng)求。這里可能有很多的源數(shù)據(jù),很多來(lái)自客戶端的數(shù)據(jù)請(qǐng)求,每個(gè)請(qǐng)求都會(huì)使服務(wù)端短暫的停止后臺(tái)計(jì)算的線程以響應(yīng)這個(gè)請(qǐng)求。所以,你必須監(jiān)控在運(yùn)行的線程數(shù)目并且保證有足夠的CPU時(shí)間來(lái)執(zhí)行必要的計(jì)算。
(譯者注:這一段在原文中是在性能優(yōu)化的章節(jié),譯者認(rèn)為這可能是作者的不小心,似乎放在線程的章節(jié)更合適。)
性能優(yōu)化
JVM的性能取決于其配置是否與應(yīng)用的功能相匹配。盡管垃圾回收器和內(nèi)存回收進(jìn)程是自動(dòng)管理內(nèi)存的,但是你必須掌管它們的頻率。通常來(lái)說(shuō),你的應(yīng)用可使用的內(nèi)存越多,那么這些會(huì)導(dǎo)致應(yīng)用暫停的內(nèi)存管理進(jìn)程需要起作用的就越少。
如果垃圾回收發(fā)生的頻率比你想的要多很多,那么可以在啟動(dòng)JVM的時(shí)候?yàn)槠渑渲酶蟮淖畲蠖汛笮≈怠6驯惶顫M的時(shí)間越久,就越能降低垃圾回收發(fā)生的頻率。最大堆大小值可以在啟動(dòng)JVM的時(shí)候,用-Xmx參數(shù)來(lái)設(shè)定。默認(rèn)的最大堆大小是被設(shè)置為可用的操作系統(tǒng)內(nèi)存的四分之一,或者最小1GB。
如果問(wèn)題出在經(jīng)常重新分配內(nèi)存,那么你可以把初始化堆大小設(shè)置為和最大堆大小一樣。這就意味著JVM永遠(yuǎn)不需要為堆重新分配內(nèi)存。但這樣做就會(huì)失去動(dòng)態(tài)堆大小適配的優(yōu)化,堆的大小從一開(kāi)始就被固定下來(lái)。配置初始化對(duì)大小是在啟動(dòng)JVM,用-Xms來(lái)設(shè)定。默認(rèn)初始化堆大小會(huì)被設(shè)定為操作系統(tǒng)可用的物理內(nèi)存的六十四分之一,或者設(shè)置一個(gè)最小值。這個(gè)值是根據(jù)不同的平臺(tái)來(lái)確定的。
如果你清楚是哪種垃圾回收(minor gc或major gc)導(dǎo)致了性能問(wèn)題,可以在不改變整個(gè)堆大小的情況下設(shè)定新生代和老生代的大小比例。對(duì)于需要產(chǎn)生大量臨時(shí)對(duì)象的應(yīng)用,需要增大新生代的比例(當(dāng)然,后果是減小了老生代的大小)。對(duì)于長(zhǎng)生命周期對(duì)象較多的應(yīng)用,則需增大老生代的比例(自然需要減少新生代的大小)。以下幾種方法可以用來(lái)設(shè)定新生代和老生代的大小:
- 在啟動(dòng)JVM時(shí),使用-XX:NewRatio參數(shù)來(lái)具體指定新生代和老生代的大小比例。比如,如果想讓老生代的大小是新生代的五倍,則設(shè)置參數(shù)為-XX:NewRatio=5,默認(rèn)這個(gè)參數(shù)設(shè)定為2(即老生代占用堆空間的三分之二,新生代占用三分之一)。
- 在啟動(dòng)JVM時(shí),直接使用-Xmn參數(shù)設(shè)定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。
- 在啟動(dòng)JVM時(shí),直接使用-XX:NewSize和-XX:MaxNewSize參數(shù)設(shè)定初始化和最大新生代大小,那么堆中的剩余大小即是老生代的大小。
每一個(gè)線程都有一個(gè)棧,用于保存函數(shù)調(diào)用、返回地址等等,這些棧有著對(duì)應(yīng)的內(nèi)存分配。如果線程過(guò)多,就會(huì)導(dǎo)致OutOfMemory錯(cuò)誤。即使你有足夠的空間的堆來(lái)存放對(duì)象,你的應(yīng)用也可能會(huì)因?yàn)閯?chuàng)建一個(gè)新的線程而崩潰。這種情況下,需要考慮限制線程中的棧大小的最大值。線程棧大小可以在JVM啟動(dòng)的時(shí)候,通過(guò)-Xss參數(shù)來(lái)設(shè)置,默認(rèn)這個(gè)值被設(shè)定為320KB至1024KB之間,這和平臺(tái)相關(guān)。
性能監(jiān)控
當(dāng)開(kāi)發(fā)或運(yùn)行一個(gè)Java應(yīng)用的時(shí)候,對(duì)JVM的性能進(jìn)行監(jiān)控是很重要的。配置JVM不是一次配置就萬(wàn)事大吉的,特別是你要應(yīng)對(duì)的是Java服務(wù)器應(yīng)用的情況。你必須持續(xù)的檢查堆內(nèi)存和非堆內(nèi)存的分配和使用情況,線程數(shù)的創(chuàng)建情況和內(nèi)存中加載的類的數(shù)據(jù)情況等。這些都是核心參數(shù)。
使用Anturis控制臺(tái),你可以為任何的硬件組件上運(yùn)行的JVM配置監(jiān)控(例如,在一臺(tái)電腦上運(yùn)行的一個(gè)Tomcat網(wǎng)頁(yè)服務(wù)器)。
JVM監(jiān)控可以使用以下衡量標(biāo)準(zhǔn):
- 總內(nèi)存使用情況(MB):即JVM使用的總內(nèi)存。如果JVM使用了所有可用內(nèi)存,這項(xiàng)指標(biāo)可以衡量底層操作系統(tǒng)的整體性能。
- 堆內(nèi)存使用(MB):即JVM為運(yùn)行的Java應(yīng)用所使用的對(duì)象分配的所有內(nèi)存。不使用的對(duì)象通常會(huì)被垃圾回收器從堆中移除。所以,如果這個(gè)指數(shù)增大,表示你的應(yīng)用沒(méi)有把不使用的對(duì)象移除或者你需要更好的配置垃圾回收器的參數(shù)。
- 非堆內(nèi)存的使用(MB):即為方法區(qū)和代碼緩存分配的所有內(nèi)存。方法區(qū)是用于存儲(chǔ)被加載的類的引用,如果這些引用沒(méi)有被適當(dāng)?shù)那謇?#xff0c;永生代池會(huì)在每次應(yīng)用被重新部署的時(shí)候都會(huì)增大,導(dǎo)致非堆的內(nèi)存泄露。這個(gè)指標(biāo)也可能指示了線程創(chuàng)建的泄露。
- 池內(nèi)總內(nèi)存(MB):即JVM所分配的所有變量?jī)?nèi)存池的內(nèi)存和(即除了代碼緩存區(qū)外的所有內(nèi)存和)。這個(gè)指標(biāo)能夠讓你明確你的應(yīng)用在JVM過(guò)載前所能使用的總內(nèi)存。
- 線程:即所有有效線程數(shù)。舉個(gè)例子,在Tomcat服務(wù)器中每個(gè)請(qǐng)求都是一個(gè)獨(dú)立的線程來(lái)處理,所以這個(gè)衡量指標(biāo)可以表示當(dāng)前有多少個(gè)請(qǐng)求數(shù),是否影響到了后臺(tái)低權(quán)限的線程的運(yùn)行。
- 類:即所有被加載的類的總數(shù)。如果你的應(yīng)用動(dòng)態(tài)的創(chuàng)建很多類,這可能是服務(wù)器內(nèi)存泄露的一個(gè)原因。
總結(jié)
- 上一篇: 面试问“请介绍下自己”,应聘者就回了两点
- 下一篇: 在谈数据治理和数字化的时候,别忘了数据标