JVM 入門

JVM性能優化(一)JVM技術入門

作者 Eva Andreasson  譯者:趙峯 校對:方騰飛  原文鏈接

Java應用程序是運行在JVM上的,但是你對JVM技術瞭解嗎?這篇文章(這個系列的第一部分)講述了經典Java虛擬機是怎麼樣工作的,例如:Java一次編寫的利弊,跨平臺引擎,垃圾回收基礎知識,經典的GC算法和編譯優化。之後的文章會講JVM性能優化,包括最新的JVM設計——支持當今高併發Java應用的性能和擴展。

如果你是一個開發人員,你肯定遇到過這樣的特殊感覺,你突然靈光一現,所有的思路連接起來了,你能以一個新的視角來回想起你以前的想法。我個人很喜歡學習新知識帶來的這種感覺。我已經有過很多次這樣的經歷了,在我使用JVM技術工作時,特別是使用垃圾回收和JVM性能優化時。在這個新的Java世界中,我希望和你分享我的這些啓發。希望你能像我寫這篇文章一樣興奮的去了解JVM的性能。
這個系列文章,是爲所有有興趣去學習,更多JVM底層知識和JVM實際做了什麼的Java開發人員所寫的。在更高層次,我將討論垃圾回收和在不影響應用運行的情況下,對空閒內存安全和速度上的無止境追求。你將學到JVM的關鍵部分:垃圾回收和GC算法,編譯優化,和一些常用的優化。我同樣會討論爲什麼Java標記會很難,並提供建議什麼時候應該考慮測試性能。最後,我將講一些JVM和GC的新的創新,包括Azul’s Zing JVM, IBM JVM, 和Oracle’s Garbage First (G1) 垃圾回收中的重點。

我希望你讀完這個系列時對Java可擴展性的限制有更深的瞭解,這樣限制是如何強制我們以最優的方式創建一個Java部署。希望你會有一種豁然開朗的感受,並且能激發了一些好的Java靈感:停止接受那些限制,並去改變它!如果你現在還不是一個開源工作者,這個系列或許會鼓勵你往這方面發展。

JVM性能調優:閱讀該系列

  • 第一部分:概述
  • 第二部分:編譯工具
  • 第三部分:垃圾回收
  • 第四部分:併發壓縮GC
  • 第五部分:可擴展性

JVM性能和“一次編譯,到處運行”的挑戰

我有新的消息告訴那些固執的認爲Java平臺本質上是緩慢的人。當Java剛剛做爲企業級應用的時候,JVM被詬病的Java性能問題已經是十幾年前的事了,但這個結論,現在已經過時了。這是真的,如果你現在在不同的開發平臺上運行簡單靜態和確定的任務時,你將很可能發現使用機器優化過的代碼,比使用任何虛擬環境執行的要好,在相同的JVM下。但是,Java的性能在過去10年有了非常大的提升。Java產業的市場需求和增長,導致了少量的垃圾回收算法、新的編譯創新、和大量的啓發式方法和優化,這些使JVM技術得到了進步。我將在以後的章節中介紹這些。

JVM的技術之美,同樣是它最大的挑戰:沒有什麼可以被認爲是“一次編譯,到處運行”的應用。不是優化一個用例,一個應用,一個特定的用戶負載,JVM不斷的跟蹤Java應用現在在做什麼,並進行相應的優化。這種動態的運行導致了一系列動態的問題。當設計創新時(至少不是在我們向生產環境要性能時),致力於JVM的開發者不會依賴靜態編譯和可預測的分配率。

JVM性能的事業

在我早期的工作中我意識到垃圾回收是非常難“解決”的,我一直着迷於JVMs和中間件技術。我對JVMs的熱情開始於我在JRockit團隊中時,編碼一種新的方法用於自學,自己調試垃圾回收算法(參考 Resources)。這個項目(轉變爲JRockit一個實驗性的特點,併成爲Deterministic Garbage Collection算法的基礎)開啓了我JVM技術的旅程。我已經在BEA系統、Intel、Sun和Oracle(因爲Oracle收購BEA系統,所以被Oracle短暫的工作過)工作過。之後我加入了在Azul Systems的團隊去管理Zing JVM,現在我爲Cloudera工作。

機器優化的代碼可能會實現較好的性能(但這是以犧牲靈活性來做代價的),但對於動態裝載和功能快速變化的企業應用,這並不是一個權衡選擇它的理由。大多數的企業爲了Java的優點,更願意去犧牲機器優化代碼,帶來完美的性能。

  • 易於編碼和功能開發(意義是更短的時間去響應市場)
  • 得到知識淵博的的程序員
  • 用Java APIs和標準庫更快速的開發
  • 可移植性——不用爲新的平臺去重新寫Java應用

從Java代碼到字節碼

做爲一個Java程序員,你可能對編碼、編譯和執行Java應用很熟悉。例子:我們假設你有一個程序(MyApp.java),現在你想讓它運行。去執行這個程序你需要先用javac(JDK內置的靜態Java語言到字節碼編譯器)編譯。基於Java代碼,javac生成相應的可執行字節碼,並保存在相同名字的class文件:MyApp.class中。在把Java代碼編譯成字節碼後,你可以通過java命令(通過命令行或startup腳本,使用不使用startup選項都可以)來啓動可執行的class文件,從而運行你的應用。這樣你的class被加載到運行時(意味着Java虛擬機的運行),程序開始執行。

這就是表面上每一個應用執行的場景,但是現在我們來探究下當你執行java命令時究竟發生了什麼。Java虛擬機是什麼?大多數開發人員通過持續調試來與JVM交互——aka selecting 和value-assigning啓動選項能讓你的Java程序跑的更快,同時避免了臭名昭著的”out of memory”錯誤。但是,你是否曾經想過,爲什麼我們起初需要一個JVM來運行Java應用呢?

什麼是Java虛擬機?

簡單的說,一個JVM是一個軟件模塊,用於執行Java應用字節碼並且把字節碼轉化到硬件,操作系統特殊指令。通過這樣做,JVM允許Java程序在第一次編寫後可以在不同的環境中執行,並不需要更改原始的代碼。Java的可移植性是通往企業應用語言的關鍵:開發者並不需要爲不同平臺重寫應用代碼,因爲JVM負責翻譯和平臺優化。

一個JVM基本上是一個虛擬的執行環境,作爲一個字節碼指令機器,而用於分配執行任務和執行內存操作通過與底層的交互。

一個JVM同樣爲運行的Java應用管理動態資源。這就意味着它掌握分配和釋放內存,在每個平臺上保持一致的線程模型,在應用執行的地方用一種適於CPU架構的方式組織可執行的指令。JVM把開發人員從需要跟蹤對象的引用和存活時長中解放出來。同樣的它不用我們管理何時去釋放內存——一個像C語言那樣的非動態語言的痛點。

你可以把JVM當做是一個專門爲Java運行的操作系統;它的工作是爲Java應用管理運行環境。一個JVM是一個通過與底層交互的虛擬執行環境,作爲一個字節碼指令機器,而用於分配執行任務和執行內存操作。

JVM組件概述

有很多寫JVM內部和性能優化的文章。作爲這個系列的基礎,我將會總結概述下JVM組件。這個簡短的閱覽會爲剛接觸JVM的開發者有特殊的幫助,會讓你瞭解之後更想深入的討論。

從一種語言到另一種——關於Java編譯器

編譯器是輸入一種語言,然後輸出另一種可執行的語句。Java編譯器有兩個主要任務:

1. 讓Java語言更加輕便,不用每次在特定的平臺上寫代碼;

2. 確保在特定的平臺產生有效的可執行的代碼。

編譯器可以是靜態也可以是動態。一個靜態編譯的例子是javac。它把Java代碼當做輸入,並轉化爲字節碼(一種在Java虛擬機執行的語言)。靜態編譯器一次解釋輸入的代碼,輸出可執行的形式,這個是在程序執行時將被用到。因爲輸入是靜態的,你將總能看到結果相同。只有當你修改原始代碼並重新編譯時,你才能看到不同的輸出。

動態編譯器,例如Just-In-Time (JIT)編譯器,把一種語言動態的轉化爲另一種,這意味着它們在運行時執行代碼。一個JIT編譯器讓你收集或創建運行數據分析(通過插入性能計數的方式實現)and make compiler decisions on the fly, using the environment data at hand(這一段不知道怎麼翻譯)。動態的編譯器在編譯的過程中,實現更好的指令序列,把一系列的指令替換成更有效的,並消除多餘的操作。隨着時間的增長你將收集更多的代碼配製數據,做更多更好的編譯決定;整個過程就是我們通常稱爲的代碼優化和重編譯。

動態編譯給了你可以根據行爲進行動態調整的優勢,或隨着應用裝載次數的增加催生的新的優化。這就是爲什麼動態編譯器非常適合Java運行。值得注意的是,動態編譯器請求外部數據結構,線程資源,CPU週期分析和優化。越深層次的優化,你將需要越多的資源。然而在大多數環境中,頂層對執行性能的提升幫助非常小——比你純粹的解釋要快5到10倍的性能。

分配會導致垃圾回收

每一個線程基於每個“Java進程分配內存地址空間” 完成內存分配,叫Java堆,或簡稱堆。在Java世界中單線程分配在客戶端應用程序中很常見。然而,單線程分配在企業應用和工作裝載服務端變的沒有任何益處,因爲它並沒有使用現在多核環境的並行優勢。

並行應用設計同樣迫使JVM保證在同一時間,多線程不會分配同一個地址空間。你可以通過在整個分配空間中放把鎖來控制。但這種技術(通常叫做堆鎖)很消耗性能,持有或排隊線程會影響資源利用和應用優化的性能。多核系統好的一面是,它們創造了一個需求,爲各種各樣的新的方法在資源分配的同時去阻止單線程的瓶頸,和序列化。

一個常用的方法是把堆分成幾部分,在對應用來說每個合式分區大小的地方——顯然它們需要調優,分配率和對象大小對不同應用來說有顯著的變化,同樣線程的數量也不同。線程本地分配緩存(Thread Local Allocation Buffer,簡寫:TLAB),或者有時,線程本地空間(Thread Local Area,簡寫:TLA),是一個專門的分區,在其中線程不用聲明一個全堆鎖就可以自由分配。當區域滿的時候,堆就滿了,表示堆上的空閒空間不夠用來放對象的,需要分配空間。當堆滿的時候,垃圾回收就會開始。

碎片

使用TLABs捕獲異常,是把堆碎片化來降低內存效率。如果一個應用在要分配對象時正巧不能增加或者不能完全分配一個TLAB空間,這將會有空間太小而不能生成新對象的風險。這樣的空閒空間被當做“碎片”。如果應用程序一直保持對象的引用,然後再用剩下的空間分配,最後這些空間會在很長一段時間內空閒。

碎片就是當碎片被分散在堆中的時候——通過一小段不用的內存空間來浪費堆空間。爲你的應用分配 “錯誤的”TLAB空間(關於對象的大小、混合對象的大小和引用持有率)是導致堆內碎片增多的原因。在隨着應用的運行,碎片的數量會增加在堆中佔有的空間。碎片導致性能下降,系統不能給新應用分配足夠的線程和對象。垃圾回收器在隨後會很難阻止out-of-memory異常。

TLAB浪費在工作中產生。一種方法可以完全或暫時避免碎片,那就是在每次基礎操作時優化TLAB空間。這種方法典型的作法是應用只要有分配行爲,就需要重新調優。通過複雜的JVM算法可以實現,另一種方法是組織堆分區實現更有效的內存分配。例如,JVM可以實現free-lists,它是連接起一串特定大小的空閒內存塊。一個連續的空閒內存塊和另一個相同大小的連續內存塊相連,這樣會創建少量的鏈表,每個都有自己的邊界。在有些情況下free-lists導致更好的合適內存分配。線程可以對象分配在一個差不多大小的塊中,這樣比你只依靠固定大小的TLAB,潛在的產生少的碎片。

GC瑣事

有一些早期的垃圾收集器擁有多個老年代,但是當超過兩個老年代的時候會導致開銷超過價值。另一種優化分配減少碎片的方法,就是創造所謂的新生代,這是一個專門用於分配新對象的專用堆空間。剩餘的堆會成爲所謂的老年代。老年代是用來分配長時間存在的對象的,被假定會存在很長時間的對象包括不被垃圾收集的對象或者大對象。爲了更好的理解這種分配的方法,我們需要講一些垃圾收集的知識。

垃圾回收和應用性能

垃圾回收是JVM的垃圾回收器去釋放沒有引用的被佔據的堆內存。當第一次觸發垃圾收集時,所有的對象引用還被保存着,被以前的引用佔據的空間被釋放或重新分配。當所有可回收的內存被收集後,空間等待被抓取和再次分配給新對象。

垃圾回收器永遠都不能重聲明一個引用對象,這樣做會破壞JVM的標準規範。這個規則的異常是一個可以捕獲的soft或weak引用 ,如果垃圾收集器將要將近耗盡內存。我強烈推薦你儘量避免weak引用,然而,因爲Java規範的模糊導致了錯誤的解釋和使用的錯誤。更何況,Java是被設計爲動態內存管理,因爲你不需要考慮什麼時候和什麼地方釋放內存。

垃圾收集器的一個挑戰是在分配內存時,需要儘量不影響運行着的應用。如果你不盡量垃圾收集,你的應用將耗近內存;如果你收集的太頻繁,你將損失吞吐量和響應時間,這將對運行的應用產生壞的影響。

GC算法

有許多不同的垃圾回收算法。稍後,在這個系列裏將深入討論幾點。在最高層,垃圾收集兩個最主要的方法是引用計數和跟蹤收集器。

引用計數收集器會跟蹤一個對象指向多少個引用。當一個對象的引用爲0時,內存將被立即回收,這是這種方法的優點之一。引用計數方法的難點在於環形數據結構和保持所有的引用即時更新。

跟蹤收集器對仍在引用的對象標記,用已經標記的對象,反覆的跟隨和標記所有的引用對象。當所有的仍然引用的對象被標記爲“live”時,所有的不被標記的空間將被回收。這種方法管理環形數據結構,但是在很多情況下收集器應該等待直到所有標記完成,在重新回收不被引用的內存之前。

有不種的途徑來被上面的方法。最著名的算法是 marking 或copying 算法, parallel 或 concurrent算法。我將在稍後的文章中討論這些。

通常來說垃圾回收的意義是致力於在堆中給新對象和老對象分配地址空間。其中“老對象”是指在許多垃圾回收後倖存的對象。用新生代來給新對象分配,老年代給老對象,這樣能通過快速回收佔據內存的短時間對象來減少碎片,同樣通過把長時間存在的對象聚合在一起,並把它們放到老年代地址空間中。所有這些在長時間對象和保存堆內存不碎片化之間減少了碎片。新生代的一個積極作用是延遲了需要花費更大代價回收老年代對象的時間,你可以爲短暫的對象重複利用相同的空間。(老空間的收集會花費更多,是因爲長時間存在的對象們,會包含更多的引用,需要更多的遍歷。)

最後值的一提的算法是compaction,這是管理內存碎片的方法。Compaction基本來說就是把對象移動到一起,從來釋放更大的連續內存空間。如果你熟悉磁盤碎片和處理它的工具,你會發現compaction跟它很像,不同的是這個運行在Java堆內存中。我將在系列中詳細討論compaction。

總結:回顧和重點

JVM允許可移植(一次編程,到處運行)和動態的內存管理,所有Java平臺的主要特性,都是它受歡迎和提高生產力的原因。

在第一篇JVM性能優化系統的文章中我解釋了一個編譯器怎麼把字節碼轉化爲目標平臺的指令語言的,並幫助動態的優化Java程序的執行。不同的應用需要不同的編譯器。

我同樣簡述了內存分配和垃圾收集,和這些怎麼與Java應用性能相關的。基本上,你越快的填滿堆和頻繁的觸發垃圾收集,Java應用的佔有率越高。垃圾收集器的一個挑戰是在分配內存時,需要儘量不影響運行着的應用,但要在應用耗盡內存之前。在以後的文章中我們會更詳細的討論傳統的和新的垃圾回收和JVM性能優化。

 

學習更多這方法的內容

  • To Collect or Not To Collect.” (Eva Andreasson, Frank Hoffmann, Olof Lindholm; JVM-02: Proceedings of the Java Virtual Machine Research and Technology Symposium, 2002): Presents the authors’ research into an adaptive decision process that determines which garbage collector technique should be invoked and how it should be applied.
  • Reinforcement Learning for a dynamic JVM” (Eva Andreasson, KTH Royal Institute of Technology, 2002): Master thesis report on how to use reinforcement learning to better optimize the decision of when to start concurrent garbage collection for a dynamic workload.
  • Deterministic Garbage Collection: Unleash the Power of Java with Oracle JRockit Real Time” (An Oracle White Paper, August 2008): Learn more about the Deterministic Garbage Collection algorithm in JRockit Real Time.
  • Why is Java faster when using a JIT vs. compiling to machine code? (Stackoverflow, December 2009): A thread discussion for learning more about Just-in-Time compiler technology.
  • Zing: A fully Java compliant highly scalable software platform that includes an application-aware resource controller and zero overhead, always-on production visibility and diagnostic tools. Zing incorporates industry-leading, proven technology to allow TBs memory heap sizes per instance with sustained throughput under dynamic load and extreme memory allocation rates common for Java applications.
  • G1: Java’s Garbage First Garbage Collector” (Eric Bruno, Dr. Dobb’s, August 2009): A good overview of GC and introduction to the G1 garbage collector.
  • Oracle JRockit: The Definitive Guide (Marcus Hirt, Marcus Lagergren; Packt Publishing, 2010): A complete guide to the JRockit JVM.

原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com

本文鏈接地址: JVM性能優化(一)JVM技術入門

發佈了29 篇原創文章 · 獲贊 2 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章