Java虛擬機JVM性能優(yōu)化(一):JVM知識總結(jié)
Java應用程序是運行在JVM上的,但是你對JVM技術了解嗎?這篇文章(這個系列的第一部分)講述了經(jīng)典Java虛擬機是怎么樣工作的,例如:Java一次編寫的利弊,跨平臺引擎,垃圾回收基礎知識,經(jīng)典的GC算法和編譯優(yōu)化。之后的文章會講JVM性能優(yōu)化,包括最新的JVM設計——支持當今高并發(fā)Java應用的性能和擴展。
如果你是一個開發(fā)人員,你肯定遇到過這樣的特殊感覺,你突然靈光一現(xiàn),所有的思路連接起來了,你能以一個新的視角來回想起你以前的想法。我個人很喜歡學習新知識帶來的這種感覺。我已經(jīng)有過很多次這樣的經(jīng)歷了,在我使用JVM技術工作時,特別是使用垃圾回收和JVM性能優(yōu)化時。在這個新的Java世界中,我希望和你分享我的這些啟發(fā)。希望你能像我寫這篇文章一樣興奮的去了解JVM的性能。
這個系列文章,是為所有有興趣去學習更多JVM底層知識,和JVM實際做了什么的Java開發(fā)人員所寫的。在更高層次,我將討論垃圾回收和在不影響應用運行的情況下,對空閑內(nèi)存安全和速度上的無止境追求。你將學到JVM的關鍵部分:垃圾回收和GC算法,編譯優(yōu)化,和一些常用的優(yōu)化。我同樣會討論為什么Java標記這樣難,提供建議什么時候應該考慮測試性能。最后,我將講一些JVM和GC的新的創(chuàng)新,包括Azul's Zing JVM, IBM JVM, 和Oracle's Garbage First (G1) 垃圾回收中的重點。
我希望你讀完這個系列時對Java可擴展性限制的特點有更深的了解,同樣的這樣限制是如何強制我們以最優(yōu)的方式創(chuàng)建一個Java部署。希望你會有一種豁然開朗的感受,并且能激發(fā)了一些好的Java靈感:停止接受那些限制,并去改變它!如果你現(xiàn)在還不是一個開源工作者,這個系列或許會鼓勵你往這方面發(fā)展。
JVM性能和“一次編譯,到處運行”的挑戰(zhàn)
我有新的消息告訴那些固執(zhí)的認為Java平臺本質(zhì)上是緩慢的人。當Java剛剛做為企業(yè)級應用的時候,JVM被詬病的Java性能問題已經(jīng)是十幾年前的事了,但這個結(jié)論,現(xiàn)在已經(jīng)過時了。這是真的,如果你現(xiàn)在在不同的開發(fā)平臺上運行簡單靜態(tài)和確定的任務時,你將很可能發(fā)現(xiàn)使用機器優(yōu)化過的代碼比使用任何虛擬環(huán)境執(zhí)行的要好,在相同的JVM下。但是,Java的性能在過去10年有了非常大的提升。Java產(chǎn)業(yè)的市場需求和增長,導致了少量的垃圾回收算法、新的編譯創(chuàng)新、和大量的啟發(fā)式方法和優(yōu)化,這些使JVM技術得到了進步。我將在以后的章節(jié)中介紹一些。
JVM的技術之美,同樣是它最大的挑戰(zhàn):沒有什么可以被認為是“一次編譯,到處運行”的應用。不是優(yōu)化一個用例,一個應用,一個特定的用戶負載,JVM不斷的跟蹤Java應用現(xiàn)在在做什么,并進行相應的優(yōu)化。這種動態(tài)的運行導致了一系列動態(tài)的問題。當設計創(chuàng)新時(至少不是在我們向生產(chǎn)環(huán)境要性能時),致力于JVM的開發(fā)者不會依賴靜態(tài)編譯和可預測的分配率。
JVM性能的事業(yè)
在我早期的工作中我意識到垃圾回收是非常難“解決”的,我一直著迷于JVMs和中間件技術。我對JVMs的熱情開始于我在JRockit團隊中時,編碼一種新的方法用于自學,自己調(diào)試垃圾回收算法(參考 Resources)。這個項目(轉(zhuǎn)變?yōu)镴Rockit一個實驗性的特點,并成為Deterministic Garbage Collection算法的基礎)開啟了我JVM技術的旅程。我已經(jīng)在BEA系統(tǒng)、Intel、Sun和Oracle(因為Oracle收購BEA系統(tǒng),所以被Oracle短暫的工作過)工作過。之后我加入了在Azul Systems的團隊去管理Zing JVM,現(xiàn)在我為Cloudera工作。
機器優(yōu)化的代碼可能會實現(xiàn)較好的性能(但這是以犧牲靈活性來做代價的),但對于動態(tài)裝載和功能快速變化的企業(yè)應用這并不是一個權衡選擇它的理由。大多數(shù)的企業(yè)為了Java的優(yōu)點,更愿意去犧牲機器優(yōu)化代碼帶來的勉強完美的性能。
1.易于編碼和功能開發(fā)(意義是更短的時間去回應市場)
2.得到知識淵博的的程序員
3.用Java APIs和標準庫更快速的開發(fā)
4.可移植性——不用為新的平臺去重新寫Java應用
從Java代碼到字節(jié)碼
做為一個Java程序員,你可能對編碼、編譯和執(zhí)行Java應用很熟悉。例子:我們假設你有一個程序(MyApp.java),現(xiàn)在你想讓它運行。去執(zhí)行這個程序你需要先用javac(JDK內(nèi)置的靜態(tài)Java語言到字節(jié)碼編譯器)編譯?;贘ava代碼,javac生成相應的可執(zhí)行字節(jié)碼,并保存在相同名字的class文件:MyApp.class中。在把Java代碼編譯成字節(jié)碼后,你可以通過java命令(通過命令行或startup腳本,使用不使用startup選項都可以)來啟動可執(zhí)行的class文件,從而運行你的應用。這樣你的class被加載到運行時(意味著Java虛擬機的運行),程序開始執(zhí)行。
這就是表面上每一個應用執(zhí)行的場景,但是現(xiàn)在我們來探究下當你執(zhí)行java命令時究竟發(fā)生了什么。Java虛擬機是什么?大多數(shù)開發(fā)人員通過持續(xù)調(diào)試來與JVM交互——aka selecting 和value-assigning啟動選項能讓你的Java程序跑的更快,同時避免了臭名昭著的”out of memory”錯誤。但是,你是否曾經(jīng)想過,為什么我們起初需要一個JVM來運行Java應用呢?
什么是Java虛擬機?
簡單的說,一個JVM是一個軟件模塊,用于執(zhí)行Java應用字節(jié)碼并且把字節(jié)碼轉(zhuǎn)化到硬件,操作系統(tǒng)特殊指令。通過這樣做,JVM允許Java程序在第一次編寫后可以在不同的環(huán)境中執(zhí)行,并不需要更改原始的代碼。Java的可移植性是通往企業(yè)應用語言的關鍵:開發(fā)者并不需要為不同平臺重寫應用代碼,因為JVM負責翻譯和平臺優(yōu)化。
一個JVM基本上是一個虛擬的執(zhí)行環(huán)境,作為一個字節(jié)碼指令機器,而用于分配執(zhí)行任務和執(zhí)行內(nèi)存操作通過與底層的交互。
一個JVM同樣為運行的Java應用照看動態(tài)資源管理。這就意味著它掌握分配和釋放內(nèi)存,在每個平臺上保持一致的線程模型,在應用執(zhí)行的地方用一種適于CPU架構的方式組織可執(zhí)行的指令。JVM把開發(fā)人員從跟蹤對象當中的引用,和它們需要在系統(tǒng)中存在多長時間中解放出來。同樣的它不用我們管理何時去釋放內(nèi)存——一個像C語言那樣的非動態(tài)語言的痛點。
你可以把JVM當做是一個專門為Java運行的操作系統(tǒng);它的工作是為Java應用管理運行環(huán)境。一個JVM基本上是一個虛擬的通過與底層的交互的執(zhí)行環(huán)境,作為一個字節(jié)碼指令機器,而用于分配執(zhí)行任務和執(zhí)行內(nèi)存操作。
JVM組件概述
有很多寫JVM內(nèi)部和性能優(yōu)化的文章。作為這個系列的基礎,我將會總結(jié)概述下JVM組件。這個簡短的閱覽會為剛接觸JVM的開發(fā)者有特殊的幫助,會讓你更想了解之后更深入的討論。
從一種語言到另一種——關于Java編譯器
編譯器是把一種語言輸入,然后輸出另一種可執(zhí)行的語句。Java編譯器有兩個主要任務:
1. 讓Java語言更加輕便,不用在第一次寫的時候固定在特定的平臺;
2. 確保對特定的平臺產(chǎn)生有效的可執(zhí)行的代碼。
編譯器可以是靜態(tài)也可以是動態(tài)。一個靜態(tài)編譯的例子是javac。它把Java代碼當做輸入,并轉(zhuǎn)化為字節(jié)碼(一種在Java虛擬機執(zhí)行的語言)。靜態(tài)編譯器一次解釋輸入的代碼,輸出可執(zhí)行的形式,這個是在程序執(zhí)行時將被用到。因為輸入是靜態(tài)的,你將總能看到結(jié)果相同。只有當你修改原始代碼并重新編譯時,你才能看到不同的輸出。
動態(tài)編譯器,例如Just-In-Time (JIT)編譯器,把一種語言動態(tài)的轉(zhuǎn)化為另一種,這意味著它們做這些時把代碼被執(zhí)行。JIT編譯器讓你收集或創(chuàng)建運行數(shù)據(jù)分析(通過插入性能計數(shù)的方式),用編譯器決定,用手邊的環(huán)境數(shù)據(jù)。動態(tài)的編譯器可以在編譯成語言的過程之中,實現(xiàn)更好的指令序列,把一系列的指令替換成更有效的,甚至消除多余的操作。隨著時間的增長你將收集更多的代碼配制數(shù)據(jù),做更多更好的編譯決定;整個過程就是我們通常稱為的代碼優(yōu)化和重編譯。
動態(tài)編譯給了你可以根據(jù)行為去調(diào)整動態(tài)的變化的優(yōu)勢,或隨著應用裝載次數(shù)的增加催生的新的優(yōu)化。這就是為什么動態(tài)編譯器非常適合Java運行。值得注意的是,動態(tài)編譯器請求外部數(shù)據(jù)結(jié)構,線程資源,CPU周期分析和優(yōu)化。越深層次的優(yōu)化,你將需要越多的資源。然而在大多數(shù)環(huán)境中,頂層對執(zhí)行性能的提升幫助非常小——比你純粹的解釋要快5到10倍的性能。
分配會導致垃圾回收
分配在每一個線程基于每個“Java進程分配內(nèi)存地址空間”,或者叫Java堆,或者直接叫堆。在Java世界中單線程分配在客戶端應用程序中很常見。然而,單線程分配在企業(yè)應用和工作裝載服務端變的沒有任何益處,因為它并沒有使用現(xiàn)在多核環(huán)境的并行優(yōu)勢。
并行應用設計同樣迫使JVM保證在同一時間,多線程不會分配同一個地址空間。你可以通過在整個分配空間中放把鎖來控制。但這種技術(通常叫做堆鎖)很消耗性能,持有或排隊線程會影響資源利用和應用優(yōu)化的性能。多核系統(tǒng)好的一面是,它們創(chuàng)造了一個需求,為各種各樣的新的方法在資源分配的同時去阻止單線程的瓶頸,和序列化。
一個常用的方法是把堆分成幾部分,在對應用來說每個合式分區(qū)大小的地方——顯然它們需要調(diào)優(yōu),分配率和對象大小對不同應用來說有顯著的變化,同樣線程的數(shù)量也不同。線程本地分配緩存(Thread Local Allocation Buffer,簡寫:TLAB),或者有時,線程本地空間(Thread Local Area,簡寫:TLA),是一個專門的分區(qū),在其中線程不用聲明一個全堆鎖就可以自由分配。當區(qū)域滿的時候,堆就滿了,表示堆上的空閑空間不夠用來放對象的,需要分配空間。當堆滿的時候,垃圾回收就會開始。
碎片
使用TLABs捕獲異常,是把堆碎片化來降低內(nèi)存效率。如果一個應用在要分配對象時正巧不能增加或者不能完全分配一個TLAB空間,這將會有空間太小而不能生成新對象的風險。這樣的空閑空間被當做“碎片”。如果應用程序一直保持對象的引用,然后再用剩下的空間分配,最后這些空間會在很長一段時間內(nèi)空閑。
碎片就是當碎片被分散在堆中的時候——通過一小段不用的內(nèi)存空間來浪費堆空間。為你的應用分配 “錯誤的”TLAB空間(關于對象的大小、混合對象的大小和引用持有率)是導致堆內(nèi)碎片增多的原因。在隨著應用的運行,碎片的數(shù)量會增加在堆中占有的空間。碎片導致性能下降,系統(tǒng)不能給新應用分配足夠的線程和對象。垃圾回收器在隨后會很難阻止out-of-memory異常。
TLAB浪費在工作中產(chǎn)生。一種方法可以完全或暫時避免碎片,那就是在每次基礎操作時優(yōu)化TLAB空間。這種方法典型的作法是應用只要有分配行為,就需要重新調(diào)優(yōu)。通過復雜的JVM算法可以實現(xiàn),另一種方法是組織堆分區(qū)實現(xiàn)更有效的內(nèi)存分配。例如,JVM可以實現(xiàn)free-lists,它是連接起一串特定大小的空閑內(nèi)存塊。一個連續(xù)的空閑內(nèi)存塊和另一個相同大小的連續(xù)內(nèi)存塊相連,這樣會創(chuàng)建少量的鏈表,每個都有自己的邊界。在有些情況下free-lists導致更好的合適內(nèi)存分配。線程可以對象分配在一個差不多大小的塊中,這樣比你只依靠固定大小的TLAB,潛在的產(chǎn)生少的碎片。
GC瑣事
有一些早期的垃圾收集器擁有多個老年代,但是當超過兩個老年代的時候會導致開銷超過價值。另一種優(yōu)化分配減少碎片的方法,就是創(chuàng)造所謂的新生代,這是一個專門用于分配新對象的專用堆空間。剩余的堆會成為所謂的老年代。老年代是用來分配長時間存在的對象的,被假定會存在很長時間的對象包括不被垃圾收集的對象或者大對象。為了更好的理解這種分配的方法,我們需要講一些垃圾收集的知識。
垃圾回收和應用性能
垃圾回收是JVM的垃圾回收器去釋放沒有引用的被占據(jù)的堆內(nèi)存。當?shù)谝淮斡|發(fā)垃圾收集時,所有的對象引用還被保存著,被以前的引用占據(jù)的空間被釋放或重新分配。當所有可回收的內(nèi)存被收集后,空間等待被抓取和再次分配給新對象。
垃圾回收器永遠都不能重聲明一個引用對象,這樣做會破壞JVM的標準規(guī)范。這個規(guī)則的異常是一個可以捕獲的soft或weak引用 ,如果垃圾收集器將要將近耗盡內(nèi)存。我強烈推薦你盡量避免weak引用,然而,因為Java規(guī)范的模糊導致了錯誤的解釋和使用的錯誤。更何況,Java是被設計為動態(tài)內(nèi)存管理,因為你不需要考慮什么時候和什么地方釋放內(nèi)存。
垃圾收集器的一個挑戰(zhàn)是在分配內(nèi)存時,需要盡量不影響運行著的應用。如果你不盡量垃圾收集,你的應用將耗近內(nèi)存;如果你收集的太頻繁,你將損失吞吐量和響應時間,這將對運行的應用產(chǎn)生壞的影響。
GC算法
有許多不同的垃圾回收算法。稍后,在這個系列里將深入討論幾點。在最高層,垃圾收集兩個最主要的方法是引用計數(shù)和跟蹤收集器。
引用計數(shù)收集器會跟蹤一個對象指向多少個引用。當一個對象的引用為0時,內(nèi)存將被立即回收,這是這種方法的優(yōu)點之一。引用計數(shù)方法的難點在于環(huán)形數(shù)據(jù)結(jié)構和保持所有的引用即時更新。
跟蹤收集器對仍在引用的對象標記,用已經(jīng)標記的對象,反復的跟隨和標記所有的引用對象。當所有的仍然引用的對象被標記為“l(fā)ive”時,所有的不被標記的空間將被回收。這種方法管理環(huán)形數(shù)據(jù)結(jié)構,但是在很多情況下收集器應該等待直到所有標記完成,在重新回收不被引用的內(nèi)存之前。
有不種的途徑來被上面的方法。最著名的算法是 marking 或copying 算法, parallel 或 concurrent算法。我將在稍后的文章中討論這些。
通常來說垃圾回收的意義是致力于在堆中給新對象和老對象分配地址空間。其中“老對象”是指在許多垃圾回收后幸存的對象。用新生代來給新對象分配,老年代給老對象,這樣能通過快速回收占據(jù)內(nèi)存的短時間對象來減少碎片,同樣通過把長時間存在的對象聚合在一起,并把它們放到老年代地址空間中。所有這些在長時間對象和保存堆內(nèi)存不碎片化之間減少了碎片。新生代的一個積極作用是延遲了需要花費更大代價回收老年代對象的時間,你可以為短暫的對象重復利用相同的空間。(老空間的收集會花費更多,是因為長時間存在的對象們,會包含更多的引用,需要更多的遍歷。)
最后值的一提的算法是compaction,這是管理內(nèi)存碎片的方法。Compaction基本來說就是把對象移動到一起,從來釋放更大的連續(xù)內(nèi)存空間。如果你熟悉磁盤碎片和處理它的工具,你會發(fā)現(xiàn)compaction跟它很像,不同的是這個運行在Java堆內(nèi)存中。我將在系列中詳細討論compaction。
總結(jié):回顧和重點
JVM允許可移植(一次編程,到處運行)和動態(tài)的內(nèi)存管理,所有Java平臺的主要特性,都是它受歡迎和提高生產(chǎn)力的原因。
在第一篇JVM性能優(yōu)化系統(tǒng)的文章中我解釋了一個編譯器怎么把字節(jié)碼轉(zhuǎn)化為目標平臺的指令語言的,并幫助動態(tài)的優(yōu)化Java程序的執(zhí)行。不同的應用需要不同的編譯器。
我同樣簡述了內(nèi)存分配和垃圾收集,和這些怎么與Java應用性能相關的?;旧?,你越快的填滿堆和頻繁的觸發(fā)垃圾收集,Java應用的占有率越高。垃圾收集器的一個挑戰(zhàn)是在分配內(nèi)存時,需要盡量不影響運行著的應用,但要在應用耗盡內(nèi)存之前。在以后的文章中我們會更詳細的討論傳統(tǒng)的和新的垃圾回收和JVM性能優(yōu)化。
相關文章
springboot的類加載器(org.springframework.boot.loader)過程詳解
這篇文章主要介紹了springboot的類加載器(org.springframework.boot.loader),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11SpringBoot如何配置MySQL和Oracl雙數(shù)據(jù)源(Mybatis)
這篇文章主要介紹了SpringBoot如何配置MySQL和Oracl雙數(shù)據(jù)源(Mybatis)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03MyBatis使用級聯(lián)操作解決lombok構造方法識別失敗問題
這篇文章主要介紹了MyBatis使用級聯(lián)操作解決lombok構造方法識別失敗問題,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07