JVM必備指南

簡介

Java虛擬機(JVM)是Java應用的運行環境,從一般意義上來講,JVM是通過規範來定義的一個虛擬的計算機,被設計用來解釋執行從Java源碼編譯而來的字節碼。更通俗地說,JVM是指對這個規範的具體實現。這種實現基於嚴格的指令集和全面的內存模型。另外,JVM也通常被形容爲對軟件運行時環境的實現。通常JVM實現主要指的是HotSpot。

JVM規範保證任何的實現都能夠以同樣的方式解釋執行字節碼。其實現可以多樣化,包括進程、獨立的Java操作系統或者直接執行字節碼的處理器芯片。我們瞭解最多的JVM是作爲軟件實現,運行在流行的操作系統平臺上(包括Windows、OS X、Linux和Solaris等)。

JVM的結構允許對一個Java應用進行更細微的控制。這些應用運行在沙箱(Sandbox)環境中。確保在沒有恰當的許可時,無法訪問到本地文件系統、處理器和網絡連接。遠程執行時,代碼還需要進行證書認證。

除了解釋執行Java字節碼,大多數的JVM實現還包含一個JIT(just-in-time 即時)編譯器,用於爲常用的方法生成機器碼。機器碼使用的是CPU的本地語言,相比字節碼有着更快的運行速度。

雖然理解JVM不是開發或運行Java程序的必要條件,但是如果多瞭解一些JVM知識,那麼就有機會避免很多性能上的問題。理解了JVM,實際上這些問題會變得簡單明瞭。

體系結構

JVM規範定義了一系列子系統以及它們的外部行爲。JVM主要有以下子系統:

  • Class Loader 類加載器。 用於讀入Java源代碼並將類加載到數據區。
  • Execution Engine 執行引擎。 執行來自數據區的指令。

數據區使用的是底層操作系統分配給JVM的內存。


類加載器(Class Loader)

JVM在下面幾種不同的層面使用不同的類加載器:

  • bootstrap class loader(引導類加載器):是其他類加載器的父類,它用於加載Java核心庫,並且是唯一一個用本地代碼編寫的類加載器。
  • extension class loader(擴展類加載器):是bootstrap class loader加載器的子類,用於加載擴展庫。
  • system class loader(系統類加載器):是extension class loader加載器的子類,用於加載在classpath中的應用程序的類文件。
  • user-defined class loader(用戶定義的類加載器):是系統類加載器或其他用戶定義的類加載器的子類。

當一個類加載器收到一個加載類的請求,首先它會檢查緩存,確認該類是否已經被加載,然後把請求代理給它的父類。如果父類沒能成功的加載類,那麼子類就會自己去嘗試加載該類。子類可檢查父類加載器的緩存,但父類不能看到子類所加載的類。之所類加載體系會這樣設計,是認爲一個子類不應該重複加載已經被父類加載過的類。

執行引擎(Execution Engine)

執行引擎一個接一個地執行被加載到數據區的字節碼。爲了保證字節碼指令對於機器來說是可讀的,執行引擎使用下面兩個方法:

  • 解釋執行:執行引擎把它遇到的每一條指令解釋爲機器語言。
  • 即時編譯:如果一條指令經常被使用,執行引擎會把它編譯爲本地代碼並存儲在緩存中。這樣,所有和這個方法相關的代碼都會直接執行,從而避免重複解釋。

儘管即時編譯比解釋執行要佔用更多的時間,但是對於需要使用成千上萬次的方法,只需要處理一次。相比每次都解釋執行,以本地代碼的方式運行會節約很多執行時間。

JVM規範中並不規定一定要使用即時編譯。即時編譯也不是用於提高JVM性能的唯一的手段。規範僅僅規定了每條字節碼對應的本地代碼,至於執行引擎如何實現這一對應過程的,完全由JVM的具體實現來決定。

內存模型(Memory Model)

Java內存模型建立在自動內存管理的概念之上。當一個對象不再被一個應用所引用,垃圾回收器就會回收它,從而釋放相應的內存。這一點和其他很多需要自行釋放內存的語言有很大不同。

JVM從底層操作系統中分配內存,並將它們分爲以下幾個區域:

  • 堆空間(Heap Space):這是共享的內存區域,用於存儲可以被垃圾回收器回收的對象。
  • 方法區(Method Area):這塊區域以前被稱作“永生代”(permanent generation),用於存儲被加載的類。這塊區域最近被JVM取消了。現在,被加載的類作爲元數據加載到底層操作系統的本地內存區。
  • 本地區(Native Area):這個區域用於存儲基本類型的引用和變量。

一個有效的管理內存方法是把對空間劃分爲不同代,這樣垃圾回收器就不用掃描整個堆區。大多數的對象的生命週期都很段短暫,那些生命週期較長的對象往往直到應用退出才需要被清除。

當一個Java應用創建了一個對象,這個對象是被存儲到“初生池”(eden pool)。一旦初生池存儲滿了,就會在新生代觸發一次minor gc(小範圍的垃圾回收)。首先,垃圾回收器會標記出那些“死對象”(不再被應用所引用的對象),同時延長所有保留對象的生命週期(這個生命週期長度是用數字來描述,代表了期所經歷過的垃圾回收的次數)。然後,垃圾回收器會回收這些死對象,並把剩餘的活着的對象移動到“倖存池”(survivor pool),從而清空初生池。

當一個對象存活達到一定的週期後,它就會被移動到堆中的老生代:“終身代”(tenured pool)。最後,當終身代被填滿時,就會觸發一次full gc或major gc(完全的垃圾回收),以清理終身代。

(譯者注:一般我們把初生池和倖存池所在的區域合併成爲新生代,把終身代所在的區域成爲老生代。對應的,在新生代上產生的gc稱爲minor gc,在老生代上產生的gc稱爲full gc。希望這樣大家在其他地方看到對應的術語時能更好理解)

當垃圾回收(gc)執行的時候,所有應用線程都要被停止,系統產生一次暫停。minor gc非常頻繁,所以被優化的能夠快速的回收死對象,是新生代的內存的主要的回收方式。major gc運行起來就相對慢得多,因爲要掃描非常多的活着的對象。垃圾回收器本身也有多種實現,有些垃圾回收器在一定情況下能更快的執行major gc。

堆的大小是動態的,只有堆需要擴張的時候纔會從內存中分配。當堆被填滿時,JVM會重新給堆分配更多的內存,直到達到堆大小的上限,這種重新分配同樣會導致應用的短暫停止。

線程

JVM是運行在一個獨立的進程中的,但它可以併發執行多個線程,每個線程都運行自己的方法,這是Java必備的一個部分。以即時消息客戶端這樣一個應用爲例,它至少運行兩個線程。一個線程用於等待用戶輸入,另一個檢查服務端是否有新的消息傳輸。再以服務端應用爲例,有時一個請求可能要涉及多個線程併發執行,所以需要多線程來處理請求。

在JVM的進程中,所有的線程共享內存和其他可用的資源。每一個JVM進程在進入點(main方法)處都要啓動一個主線程,其他線程都從主線程啓動,成爲執行過程中的一個獨立部分。線程可以再不同的處理器上並行執行,同樣也可以共享一個處理器,線程調度器負責處理多個線程共享一個處理器的情況。

很多應用(特別是服務端應用)會處理很多任務,需要並行運行。這些任務中有些是非常重要的,需要實時執行的。而另外一些是後臺任務,可以在CPU空閒時執行。任務是在不同的線程中運行的。舉例子來說,服務端可能有一些低優先級的線程,它們會根據一些數據來計算統計信息。同時也會啓動一些高優先級的進程用於處理傳入的數據,響應對這些統計信息的請求。這裏可能有很多的源數據,很多來自客戶端的數據請求,每個請求都會使服務端短暫的停止後臺計算的線程以響應這個請求。所以,你必須監控在運行的線程數目並且保證有足夠的CPU時間來執行必要的計算。

(譯者注:這一段在原文中是在性能優化的章節,譯者認爲這可能是作者的不小心,似乎放在線程的章節更合適。)

性能優化

JVM的性能取決於其配置是否與應用的功能相匹配。儘管垃圾回收器和內存回收進程是自動管理內存的,但是你必須掌管它們的頻率。通常來說,你的應用可使用的內存越多,那麼這些會導致應用暫停的內存管理進程需要起作用的就越少。

如果垃圾回收發生的頻率比你想的要多很多,那麼可以在啓動JVM的時候爲其配置更大的最大堆大小值。堆被填滿的時間越久,就越能降低垃圾回收發生的頻率。最大堆大小值可以在啓動JVM的時候,用-Xmx參數來設定。默認的最大堆大小是被設置爲可用的操作系統內存的四分之一,或者最小1GB。

如果問題出在經常重新分配內存,那麼你可以把初始化堆大小設置爲和最大堆大小一樣。這就意味着JVM永遠不需要爲堆重新分配內存。但這樣做就會失去動態堆大小適配的優化,堆的大小從一開始就被固定下來。配置初始化對大小是在啓動JVM,用-Xms來設定。默認初始化堆大小會被設定爲操作系統可用的物理內存的六十四分之一,或者設置一個最小值。這個值是根據不同的平臺來確定的。

如果你清楚是哪種垃圾回收(minor gc或major gc)導致了性能問題,可以在不改變整個堆大小的情況下設定新生代和老生代的大小比例。對於需要產生大量臨時對象的應用,需要增大新生代的比例(當然,後果是減小了老生代的大小)。對於長生命週期對象較多的應用,則需增大老生代的比例(自然需要減少新生代的大小)。以下幾種方法可以用來設定新生代和老生代的大小:

  • 在啓動JVM時,使用-XX:NewRatio參數來具體指定新生代和老生代的大小比例。比如,如果想讓老生代的大小是新生代的五倍,則設置參數爲-XX:NewRatio=5,默認這個參數設定爲2(即老生代佔用堆空間的三分之二,新生代佔用三分之一)。
  • 在啓動JVM時,直接使用-Xmn參數設定初始化和最大新生代大小,那麼堆中的剩餘大小即是老生代的大小。
  • 在啓動JVM時,直接使用-XX:NewSize-XX:MaxNewSize參數設定初始化和最大新生代大小,那麼堆中的剩餘大小即是老生代的大小。

每一個線程都有一個棧,用於保存函數調用、返回地址等等,這些棧有着對應的內存分配。如果線程過多,就會導致OutOfMemory錯誤。即使你有足夠的空間的堆來存放對象,你的應用也可能會因爲創建一個新的線程而崩潰。這種情況下,需要考慮限制線程中的棧大小的最大值。線程棧大小可以在JVM啓動的時候,通過-Xss參數來設置,默認這個值被設定爲320KB至1024KB之間,這和平臺相關。

性能監控

當開發或運行一個Java應用的時候,對JVM的性能進行監控是很重要的。配置JVM不是一次配置就萬事大吉的,特別是你要應對的是Java服務器應用的情況。你必須持續的檢查堆內存和非堆內存的分配和使用情況,線程數的創建情況和內存中加載的類的數據情況等。這些都是核心參數。

使用Anturis控制檯,你可以爲任何的硬件組件上運行的JVM配置監控(例如,在一臺電腦上運行的一個Tomcat網頁服務器)。

JVM監控可以使用以下衡量標準:

  • 總內存使用情況(MB):即JVM使用的總內存。如果JVM使用了所有可用內存,這項指標可以衡量底層操作系統的整體性能。
  • 堆內存使用(MB):即JVM爲運行的Java應用所使用的對象分配的所有內存。不使用的對象通常會被垃圾回收器從堆中移除。所以,如果這個指數增大,表示你的應用沒有把不使用的對象移除或者你需要更好的配置垃圾回收器的參數。
  • 非堆內存的使用(MB):即爲方法區和代碼緩存分配的所有內存。方法區是用於存儲被加載的類的引用,如果這些引用沒有被適當的清理,永生代池會在每次應用被重新部署的時候都會增大,導致非堆的內存泄露。這個指標也可能指示了線程創建的泄露。
  • 池內總內存(MB):即JVM所分配的所有變量內存池的內存和(即除了代碼緩存區外的所有內存和)。這個指標能夠讓你明確你的應用在JVM過載前所能使用的總內存。
  • 線程:即所有有效線程數。舉個例子,在Tomcat服務器中每個請求都是一個獨立的線程來處理,所以這個衡量指標可以表示當前有多少個請求數,是否影響到了後臺低權限的線程的運行。
  • 類:即所有被加載的類的總數。如果你的應用動態的創建很多類,這可能是服務器內存泄露的一個原因。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章