JVM概論

引子 java

  Java虛擬機是Java應用程序的執行環境。一般而言,JVM是由一組嚴格的指令集和一個複雜的內存模型來具體實現的虛擬機,它用來解釋編譯好的java字節碼文件,將字節碼轉換爲特定機器能夠執行的本機代碼(native code)。它也能夠指代某一軟件運行時的進程實例。這裏,咱們以hotspot實現的JVM爲例。 算法

  JVM的規則保證任何一款具體實現的JVM都要以徹底相同的方式去解釋java字節碼文件,不管是一個進程,一個獨立的java操做系統,抑或是一個直接執行字節碼命令的處理器芯片。通常狀況下,咱們一般討論的JVM是一個運行在操做系統上的進程。 編程

  JVM的架構設計使得它能夠精細的控制JAVA應用程序的每個動做,在沒有權限的狀況下,應用程序沒法去訪問本地文件系統,處理器,網絡等。例如,在遠程操做的狀況下,代碼須要有簽名證書。 bootstrap

  除了去解釋java字節碼,許多軟件實現的JVM都有一個JIT編譯器用於生成頻繁執行的方法機器代碼。機器代碼是能夠直接被cpu解析執行的,因此比字節碼速度更快。 小程序

你無需去理解JVM的內部,就能編寫並運行一個JAVA應用程序。可是,若是你知道了其中的一些原理,就能避免一些性能上的問題。本文以sunspot爲例子來講明。 數組

架構 緩存

  JVM主要有兩大子系統: 網絡

  • 類加載器:負責讀取java源文件,並把類加載進內存。
  • 執行引擎:負責執行指令。

  這裏的內存是底層操做系統分配給JVM的,以下所示: 架構

類加載器 併發

  JVM應用不一樣類型的類加載器構造了層次結構:

  • bootstrap類加載器,它是其餘類加載器的父母,負責加載核心java類庫,而且是惟一的一個使用本機代碼進行編寫的類加載器。
  • 擴展類加載器,它是bootstrap類加載器的孩子,負責加載擴展類庫。
  • 系統類加載器,它是擴展類加載器的的孩子, 負責從classpath中加載類文件。
  • 用戶自定義類加載器,它是系統類加載器或其餘用戶自定義類加載器的孩子。

  當類加載器收到去加載一個類的請求時,會去檢查cache中該類是否已經被加載,而後向其父加載器發出加載請求,若是其父加載器加載失敗,那麼它就本身進行加載。一個子類加載器能夠檢查其父類加載器的cache中是否加載了某個類,可是父類加載器沒法查看子類cache中的緩存。這樣設計的緣由是爲了防止子類加載器加載那些已經被父類加載器加載過的類。(呼,好繞口。。。)

  java文件通過編譯後會生成.class字節碼文件,它定義了JVM中的一個類型,包括域,方法,繼承信息,註解和其餘元數據。咱們知道,類是JVM能加載的最小程序代碼單元,將一個新的類加入到當前運行中的JVM中,首先要對類文件進行加載和鏈接,而後將一個表明該類的Class對象交給JVM,才能夠建立新的實例。

 

加載與鏈接

 

  JVM要執行.class文件中的字節碼,首先必須以字節流的方式將文件讀入,而後轉化爲可使用的格式加入到運行的JVM中。這兩步被稱爲加載與鏈接。

 

加載

 

  這個過程首先會建立一個字節數組,而後從文件系統中讀取構成類文件的字節流,最後產生與所加載類對應的Class對象。這個過程當中會對類作一些基本檢查,加載結束後,Class對象還不完整,因此類是不可用的。

 

鏈接

 

  加載工做完成後,類須要被鏈接起來,這裏分爲3個階段:

 

  • 驗證:正式類文件符合預期,不會引發運行時錯誤或其餘問題。主要包括完整性檢查,常量池檢查,字節碼檢查。

 

  • 準備:在類文件中引用的其餘類型須要所有定位到,分配內存確保該類準備就緒。此時並不會初始化變量,也不會執行任何字節碼。

 

  • 解析:解析會促使JVM檢查類文件中引用到的類型是否都是已知類型,若是此時有未被加載進來的類,則會觸發新的加載過程。

 

  • 初始化:初始化靜態變量,靜態初始化代碼塊運行,類可使用了。

  鏈接與加載的最終產物是一個Class對象,它能夠表示加載並鏈接起來的新類型,能夠用它來建立新實例。

 

執行引擎

  執行引擎負責執行被加載進內存的字節碼指令,爲了使計算機可以識別字節碼,執行引擎採用了兩種方式:

  • 解釋:執行引擎將字節碼解釋成機器語言。
  • JIT編譯:若是一個方法被頻繁的執行,執行引擎會把該方法的字節碼編譯成本機代碼存於cache中,因而,全部與該方法相關的指令都無需解釋,直接執行。

  儘管JIT的編譯過程比普通的解釋過程要耗時,可是它只需編譯一次,對於那些上千次調用的方法來講,直接執行機器代碼就比每次都要轉換字節碼再執行要划算了。

  JIT編譯器對於JVM而言並不是是必須的組件,同時,也不是提高JVM性能的惟一手段。JVM規範只是定義了字節碼與機器代碼的對應關係,至於如何具體實現,就是不一樣版本JVM的事情了。

內存模型

  JAVA內存模型是創建在內存自動管理機制之上的。當一個對象不在被應用程序引用,垃圾收集器GC就丟棄它並釋放內存。這與其餘編程語言須要手動釋放對象的方式不一樣。

  JVM從操做系統中申請來內存,並分割成以下幾個區域:

  • 堆:存放對象實例的共享區,GC會來進行掃描。
  • 方法區:用於存放類加載器加載進來的字節碼,靜態變量,常量等等。最近,它被JVM移除,類被當作元數據加載進本機操做系統的內存。
  • 棧:存放基本類型變量和類對象實例的引用,線程私有。

垃圾回收

  內存自動管理是JAVA平臺最重要的組成部分。一個java進程既有棧又有堆,其中,棧保存了基本類型的局部變量,以及自定義類型變量在堆中存放的地址。堆中保存了要建立的對象。java對堆內存回收和再利用的基本算法被稱爲標記和清除。

 

  最簡單的標記和清除算法首先會暫停全部正在運行的線程,而後堆中遍歷引用樹,標記出「活」的對象,遍歷完成後則清除回收全部未被標記的對象。其中,「活」的對象是指在任意用戶線程的棧幀中存在引用的對象。被清除的內存並不會還給OS,而是交給JVM。

  JAVA對標記清除算法作了改進,採用「分代式垃圾收集」方法,由於對象的生存期或者很短或者很長,因此根據對象的生命週期將堆內存劃分爲不一樣區域,充分利用對象生命週期的特色。所以,同一個對象在其不一樣生命週期中,對它的引用可能指向了不一樣的內存區域。

 

  將堆根據類實例的生存週期劃分爲不一樣區域使得內存管理更加有效,GC無需遍歷整個堆。絕大多數對象的生命週期都很短,而那些略長一些的對象所佔內存在程序結束以前不大可能被所有回收。

 

內存區域劃分

  • Eden區:Eden區中存放剛建立的對象,大多數對象都僅僅存在過這裏。
  • Survivor Space:這個區域一般被劃分爲兩個區域S0和S1,從Eden中倖存下來未被清除的對象會被挪到其中一個區域中。
  • tenured區:這個區域中存放從Survivor Space區域中挪過來的對象。

 

收集方式

  對不一樣區域的內存回收方式是不一樣的,具體來說主要分爲年輕代收集和徹底收集。 

 

年輕代收集

  咱們將Eden區和Survivor Space稱爲年輕代,對這部份內存的清理與收集的過程很簡單:

  • 標記:當一個類對象被建立,它會被存於堆的eden池中,當Eden池滿時就會觸發young GC。在遍歷年輕代區域的過程當中,將發現的「活」對象都標記出來並挪走。
    1. GC遍歷Eden區和Survivor Space區,標記出「活」對象並增長那些仍然存活的對象的"壽命值"(使用經歷過垃圾回收次數來表示)。而後進行Eden區+有對象的Survivor區(設爲S0區)垃圾回收,把存活的對象用複製算法拷貝到一個空的Survivor(S1)中,此時Eden區被清空,另一個Survivor S0也爲空。下次觸發Young GC回收Eden+S1,將存活對象拷貝到S0中。新生代垃圾回收簡單、粗暴、高效。(每次Young GC都會使Survivor區存活對象值+1,直到閾值)。
    2. 將Survivor Space區中S1足夠老(被標記次數足夠多)的對象挪進tenured區。
  • 清除:最後,清空Eden區和S1區就能夠重用了。

 

徹底收集

  當tenured區滿了,年輕代收集就沒法把對象放入tenured區了,這時候會觸發一次徹底收集。根據老年代所用的垃圾收集器,對老年代對象進行內部遷移。

 

發生一次 Major GC 至少伴隨一次Young GC,通常比JVM在tenured區申請不到內存,會進行Full GC。tenured區使用通常採用Concurrent-Mark–Sweep策略回收內存。

當一個GC運行時,應用程序全部的進程都將中止。Young GC很頻繁,可是會很快清理Eden池中的對象。而Major GC因爲涉及到大量仍存活的對象,因此比Young GC慢不少。

堆內存是動態的。當堆內存滿時,JVM會從新分配內存給它直到最大限度,同時也中止應用程序進程來完成內存分配。

線程

  JVM是一個單進程,可是它能夠併發多個線程,不一樣線程執行本身的方法。全部的線程共享着JVM分配到的資源。JVM進程在程序入口(main方法)新開一個線程,其他的線程都來自與此線程,並獨立執行。多個線程能夠併發地在不一樣處理器中執行,或者共享同一個處理器。

相關文章
相關標籤/搜索