衆所周知,Java應用程序是運行在JVM上的,可是你對JVM有所瞭解麼?做爲這個系列文章的第一篇,本文將對經典Java虛擬機的運行機制作簡單介紹,內容包括「一次編寫,處處運行」的利弊、垃圾回收的基本原理、經常使用垃圾回收算法的示例和編譯器優化等。後續的系列文章將會JVM性能優化的內容進行介紹,包括新一代JVM的設計思路,以及如何支持當今Java應用程序對高性能和高擴展性的要求。html
若是你是一名程序員,那麼毫無疑問,你確定有過某種興奮的感受,就像是當一束靈感之光照亮了你思考方向,又像是神經元最終創建鏈接,又像是你解放思想開拓了新的局面。就我我的來講,我喜歡這種學習新知識的感受。我在工做時就經常會有這種感受,個人工做會涉及到一些JVM的相關技術,這着實令我興奮,尤爲是工做涉及到垃圾回收和JVM性能優化的時候。在這個系列中,我但願能夠與你分享一些這方面的經驗,但願你也會像我同樣熱愛JVM相關技術。java
這個系列文章主要面向那些想要裂解JVM底層運行原理的Java程序員。文章立足於較高的層面展開討論,內容涉及到垃圾回收和在不影響應用程序運行的狀況下對安全快速的釋放/分配內存。你將對JVM的核心模塊有所瞭解:垃圾回收、GC算法、編譯器行爲,以及一些經常使用優化技巧。此外,還會討論爲何對Java作基準測試(benchmark)是件很困難的事,並提供一些建議來幫助作基準測試。最後,將會介紹一些JVM和GC的前沿技術,內容涉及到Azul的Zing JVM,IBM JVM和Oracle的Garbage First(G1)垃圾回收器。git
但願在閱讀此係列文章後,你能對影響Java伸縮性的因素有所瞭解,而且知道這些因素是如何影響Java開發的,如何使Java難以優化的。但願會你有那種發自心裏的驚歎,而且可以激勵你爲Java作一點事情:拒絕限制,努力改變。若是你還沒準備好爲開源事業貢獻力量,但願本系列文章能夠爲你指明方向。程序員
JVM職業生涯github
在我職業生涯的早期,垃圾回收的問題曾經很難解決。垃圾回收問題和JVM的跨平臺問題我更加爲JVM和中間件的相關技術而着迷。我對JVM的熱情源於十年前在JRockit團隊工做的經歷,當時要編碼實現一種新的、可以自動學習、自動調優的垃圾回收算法(參見相關資源)。從那個項目開始,我踏上了JVM技術之旅,期間在BEA System公司工做的不少年,與Intel公司和Sun公司有過合做關係,在Oracle收購BEA公司和Sun公司以後爲Oracle工做了一年。另外,個人碩士論文深刻分析了JRockit的試驗性特性,爲Deterministic Garbage Collection算法打下了基礎。當我加入Azul公司的團隊後,我回到了熟悉的工做中,負責管理維護Zing JVM的垃圾回收算法。如今個人工做有了一點小變化,負責日程安排與資源管理,關注分佈式的可伸縮數據處理框架,目前在Cloudera公司工做,負責開源項目Hadoop的開發。算法
有很多人認爲,Java平臺自己就挺慢。其主要觀點簡單來講就是,Java性能低已經有些年頭了 ―― 最先能夠追溯到Java第一次用於企業級應用程序開發的時候。但這早就是老黃曆了。事實是,若是你對不一樣的開發平臺上運行簡單的、靜態的、肯定性任務的運行結果作比較,你就會發現使用通過機器級優化(machine-optimized)代碼的平臺比任何使用虛擬環境進行運算的都要強,JVM也不例外。可是,在過去的10年中,Java的性能有了大幅提高。市場上不斷增加的需求催生了垃圾回收算法的出現和編譯技術的革新,在不斷探索與優化的過程當中,JVM茁壯成長。在這個系列文章中,我將介紹其中的一些內容。編程
JVM技術中最迷人的地方也正是其最具挑戰性的地方:「一次編寫,處處運行」。JVM並不對具體的用例、應用程序或用戶負載進行優化,而是在應用程序運行過程當中不斷收集運行時信息,並以此爲根據動態的進行優化。這種動態的運行時特性帶來了不少動態問題。在設計優化方案時,以JVM爲工做平臺的程序沒法依靠靜態編譯和可預測的內存分配速率(predictable allocation rates)對應用程序作性能評估,至少在對生產環境進行性能評估時是不行的。安全
機器級優化過的代碼有時能夠達到更好的性能,但它是以犧牲可移植性爲代價的,在企業級應用程序中,動態負載和快速迭代更新是更加劇要的。大多數企業會願意犧牲一點機器級優化代碼帶來的性能,以此換取Java平臺的諸多優點:性能優化
做爲一名Java程序員,你能夠已經對編碼、編譯和運行這一套流程比較熟悉了。假如說,如今你寫了一個程序代碼MyApp.java,準備編譯運行。爲了運行這個程序,首先,你須要使用JDK內建的Java語言編譯器,javac,對這個文件進行編譯,它能夠將Java源代碼編譯爲字節碼。javac將根據Java程序的源代碼生成對應的可執行字節碼,並將其保存爲同名類文件:MyApp.class。在通過編譯階段後,你就能夠在命令行中使用java命令或其餘啓動腳本載入可執行的類文件來運行程序,而且能夠爲程序添加啓動參數。以後,類會被載入到運行時(這裏指的是正在運行的JVM),程序開始運行。服務器
上面所描述的就是在運行Java應用程序時的表面過程,但如今,咱們要深刻挖掘一下,在調用Java命令時,到底發生了什麼?JVM究竟是什麼?大多數程序員是經過不斷的調優,即便用相應的啓動參數,與JVM進行交互,使Java程序運行的更快,同時避免程序出現「out of memory」錯誤。但你是否想過,爲何咱們必需要經過JVM來運行Java應用程序呢?
簡單來講,JVM是用於執行Java應用程序和字節碼的軟件模塊,而且能夠將字節碼轉換爲特定硬件和特定操做系統的本地代碼。正因如此,JVM使Java程序作到了「一次編寫,處處運行」。Java語言的可移植性是獲得企業級應用程序開發者青睞的關鍵:開發者無需因平臺不一樣而把程序從新編寫一遍,由於有JVM負責處理字節碼到本地代碼的轉換和平臺相關優化的工做。
基本上來講,JVM是一個虛擬運行環境,對於字節碼來講就像是一個機器同樣,能夠執行任務,並經過底層實現執行內存相關的操做。
JVM也能夠在運行java應用程序時,很好的管理動態資源。這指的是他能夠正確的分配、回收內存,在不一樣的上維護一個具備一致性的線程模型,而且能夠爲當前的CPU架構組織可執行指令。JVM解放了程序員,使程序員沒必要再關係對象的生命週期,使程序員沒必要再關心應該在什麼時候釋放內存。而這,正是使用着相似C語言的非動態語言的程序員心中永遠的痛。
你能夠將JVM當作是一種專爲Java而生的特殊的操做系統,它的工做是管理運行Java應用程序的運行時環境。簡單來講,JVM就是運行字節碼指令的虛擬執行環境,而且能夠分配執行任務,或經過底層實現對內存進行操做。
關於JVM內部原理與性能優化有不少內容可寫。做爲這個系列的開篇文章,我簡單介紹JVM的內部組件。這個簡要介紹對於那些JVM新手比較有幫助,也是爲後面的深刻討論作個鋪墊。
編譯器
以一種語言爲輸入,生成另外一種可執行語言做爲輸出。Java編譯器主要完成2個任務:
編譯器能夠是靜態的,也能夠是動態的。靜態編譯器,如javac,它以Java源代碼爲輸入,將其編譯爲字節碼(一種能夠運行JVM中的語言)。*靜態編譯器*解釋輸入的源代碼,而生成可執行輸出代碼則會在程序真正運行時用到。由於輸入是靜態的,全部輸出結果老是相同的。只有當你修改的源代碼並從新編譯時,纔有可能看到不一樣的編譯結果。
動態編譯器,如使用Just-In-Time(JIT,即時編譯)技術的編譯器,會動態的將一種編程語言編譯爲另外一種語言,這個過程是在程序運行中同時進行的。JIT編譯器會收集程序的運行時數據(在程序中插入性能計數器),再根據運行時數據和當前運行環境數據動態規劃編譯方案。動態編譯能夠生成更好的序列指令,使用更有效率的指令集合替換原指令集合,或剔除冗餘操做。收集到的運行時數據的越多,動態編譯的效果就越好;這一般稱爲代碼優化或重編譯。
動態編譯使你的程序能夠應對在不一樣負載和行爲下對新優化的需求。這也是爲何動態編譯器很是適合Java運行時。這裏須要注意的地方是,動態編譯器須要動用額外的數據結構、線程資源和CPU指令週期,才能收集運行時信息和優化的工做。若想完成更高級點的優化工做,就須要更多的資源。可是在大多數運行環境中,相對於得到的性能提高來講,動態編譯的帶來的性能損耗實際上是很是小的 ―― 動態編譯後的代碼的運行效率能夠比純解釋執行(即按照字節碼運行,不作任何修改)快5到10倍。
內存分配
是以線程爲單位,在「Java進程專有內存地址空間」中,也就是Java堆中分配的。在普通的客戶端Java應用程序中,內存分配都是單線程進行的。可是,在企業級應用程序和服務器端應用程序中,單線程內存分配卻並非個好辦法,由於它沒法充分利用現代多核時代的並行特性。
並行應用程序設計要求JVM確保多線程內存分配不會在同一時間將同一塊地址空間分配給多個線程。你能夠在整個內存空間中加鎖來解決這個問題,可是這個方法(即所謂的「堆鎖」)開銷較大,由於它迫使全部線程在分配內存時逐個執行,對資源利用和應用程序性能有較大影響。多核程序的一個額外特色是須要有新的資源分配方案,避免出現單線程、序列化資源分配的性能瓶頸。
經常使用的解決方案是將堆劃分爲幾個區域,每一個區域都有適當的大小,固然具體的大小須要根據實際狀況作相應的調整,由於不一樣應用程序之間,內存分配速率、對象大小和線程數量的差異是很是大的。Thread Local Allocation Buffer(TLAB),有時也稱爲Thraed Local Area(TLA),是線程本身使用的專用內存分配區域,在使用的時候無需獲取堆鎖。當這個區域用滿的時候,線程會申請新的區域,直到堆中全部預留的區域都用光了。當堆中沒有足夠的空間來分配內存時,堆就「滿」了,即堆上剩餘的空間裝不下待分配空間的對象。當堆滿了的時候,垃圾回收就開始了。
使用TLAB的一個風險是,因爲堆上內存碎片的增長,使用內存的效率會降低。若是應用程序建立的對象的大小沒法填滿TLAB,而這塊TLAB中剩下的空間又過小,沒法分配給新的對象,那麼這塊空間就被浪費了,這就是所謂的「碎片」。若是「碎片」周圍已分配出去的內存長時間沒法回收,那麼這塊碎片研究長時間沒法獲得利用。
碎片化
是指堆上存在了大量的碎片
,因爲這些小碎片的存在而使堆沒法獲得有效利用,浪費了堆空間。爲應用程序設置TLAB的大小時,如果沒有對應用程序中對象大小和生命週期和合理評估,致使TLAB的大小設置不當,就會是使堆逐漸碎片化。隨着應用程序的運行,被浪費的碎片空間會逐漸增多,致使應用程序性能降低。這是由於系統沒法爲新線程和新對象分配空間,因而爲防止出現OOM(out-of-memory)錯誤,而頻繁GC的緣故。
對於TLAB產生的空間浪費這個問題,能夠採用「曲線救國」的策略來解決。例如,能夠根據應用程序的具體環境調整TLAB的大小。這個方法既能夠臨時,也能夠完全的避免堆空間的碎片化,但須要隨着應用程序內存分配行爲的變化而修改TLAB的值。此外,還可使用一些複雜的JVM算法和其餘的方法來組織堆空間來得到更有效率的內存分配行爲。例如,JVM能夠實現空閒列表(free-list),空閒列表中保存了堆中指定大小的空閒塊。具備相似大小空閒塊保存在一個空閒列表中,所以能夠建立多個空閒列表,每一個空閒列表保存某個範圍內的空閒塊。在某些事例中,使用空閒列表會比使用按實際大小分配內存的策略更有效率。線程爲某個對象分配內存時,能夠在空閒列表中尋找與對象大小最接近的空間塊使用,相對於使用固定大小的TLAB,這種方法更有利於避免碎片化的出現。
GC往事
早期的垃圾回收器有多個老年代,但實際上,存在多個老年代是弊大於利的。
另外一種對抗碎片化的方法是建立一個所謂的年輕代,在這個專有的堆空間中,保存了全部新建立的對象。堆空間中剩餘的空間就是所謂的老年代。老年代用於保存具備較長生命週期的對象,即當對象可以挺過幾輪GC而不被回收,或者對象自己很大(通常來講,大對象都具備較長的壽命週期)時,它們就會被保存到老年代。爲了讓你可以更好的理解這個方法,咱們有必要談談垃圾回收。
垃圾回收就是JVM釋放那些沒有引用指向的堆內存的操做。當垃圾回收首次觸發時,有引用指向的對象會被保存下來,那些沒有引用指向的對象佔用的空間會被回收。當全部可回收的內存都被回收後,這些空間就能夠被分配給新的對象了。
垃圾回收不會回收仍有引用指向的對象;不然就會違反JVM規範。這個規則有一個例外,就是對軟引用或弱引用的使用,當垃圾回收器發現內存快要用完時,會回收只有軟引用或弱引用指向的對象所佔用的內存。個人建議是,儘可能避免使用弱引用,由於Java規範中存在的模糊的表述可能會使你對弱引用的使用產生誤解。此外,Java自己是動態內存管理的,你不必考慮何時該釋放哪塊內存。
對於垃圾回收來講,挑戰在於,如何將垃圾回收對應用程序形成的影響降到最小。若是垃圾回收執行的不充分,那麼應用程序早晚會發生OOM錯誤;若是垃圾回收執行的太頻繁,會對應用程序的吞吐量和響應時間形成影響,固然,這都不是好的影響。
目前已經出現了不少垃圾回收算法。在這個系列文章中將對其中的一些進行介紹。歸納來講,垃圾回收主要有兩種方式,引用計數(reference counting)和引用追蹤(reference tracing)。
上面提到的兩種算法有多種不一樣的實現方法,其中最著名可算是標記或拷貝算法(marking or copying algorithm)和並行或併發算法(parallel or concurrent algorithm)。我將在後續的文章中對它們進行介紹。
分代垃圾回收的意思是,將堆劃分爲幾個不一樣的區域,分別用於存儲新對象和老對象。其中「老對象」指的是挺過了幾輪垃圾回收而不死的對象。將堆空間分爲年輕代和老年代,分別用於存儲新對象和老對象能夠經過回收生命週期較短的對象,並將生命週期較長的對象從年輕代提高到老年代的方法來減小堆空間中的碎片,下降堆空間碎片化的風險。此外,使用年輕代還有一個好處是,它能夠推出對老年代進行垃圾回收的需求(對老年代進行垃圾回收的代價比較大,由於老年代中那些生命週期較長的對象一般包含有更多的引用,遍歷一次須要花費更多的時間),因那些生命週期較短的對一般會重用年輕代中的空間。
還有一個值得一提的算法改進是壓縮,它能夠用來管理堆空間中的碎片。基本上將,壓縮就是將對象移動到一塊兒,再釋放掉較大的連續空間。若是你對磁盤碎片和處理磁盤碎片的工具比較熟悉的話你就會理解壓縮的含義了,只不過這裏的壓縮是工做在Java堆空間中的。我將在該系列後續的內容中對壓縮進行介紹。
JVM實現了可移植性(「一次編寫,處處運行」)和動態內存管理,這兩個特色也是其廣受歡迎,而且具備較高生產力的緣由。
做爲這個系列文章的第一篇,我介紹了編譯器如何將字節碼轉換爲平臺相關指令的語言,以及如何動態
優化Java程序的運行性能。不一樣的編譯器迎合了不一樣應用程序的須要。
此外,簡單介紹了內存分配和垃圾回收的一點內容,及其與Java應用程序性能的關係。基本上將,Java應用程序運行的速度越快,填滿Java堆所需的時間就越短,觸發垃圾回收的頻率也越高。這裏遇到的問題就是,在應用程序出現OOM錯誤以前,如何在對應用程序形成的影響儘量小的狀況下,回收足夠多的內存空間。將後續的文章中,咱們將對傳統垃圾回收方法和現今的垃圾回收方法對JVM性能優化的影響作詳細討論。
Eva Andearsson對JVM技術、SOA、雲計算和其餘企業級中間件解決方案有着10多年的從業經驗。在2001年,她以JRockit JVM開發者的身份加盟了創業公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收領域的研究和算法方面,EVA得到了兩項專利。此外她仍是提出了肯定性垃圾回收(Deterministic Garbage Collection),後來造成了JRockit實時系統(JRockit Real Time)。在技術上,Eva與SUn公司和Intel公司合做密切,涉及到不少將JRockit產品線、WebLogic和Coherence整合的項目。2009年,Eva加盟了Azul System公,擔任產品經理。負責新的Zing Java平臺的開發工做。最近,她改換門庭,以高級產品經理的身份加盟Cloudera公司,負責管理Cloudera公司Hadoop分佈式系統,致力於高擴展性、分佈式數據處理框架的開發。
英文原文:JVM performance optimization, Part 1,翻譯:ImportNew - 曹旭東