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