引子
java虛擬機是Java應用程序的執行環境。通常而言,JVM是由一組嚴格的指令集和一個復雜的內存模型來具體實現的虛擬機,它用來解釋編譯好的java字節碼文件,將字節碼轉換為特定機器可以執行的本機代碼(native code)。它也可以指代某一軟件運行時的進程實例。這里,我們以hotspot實現的JVM為例。
JVM的規則保證任何一款具體實現的JVM都要以完全相同的方式去解釋java字節碼文件,無論是一個進程,一個獨立的java操作系統,抑或是一個直接執行字節碼命令的處理器芯片。一般情況下,我們通常討論的JVM是一個運行在操作系統上的進程。
JVM的架構設計使得它可以精細的控制JAVA應用程序的每一個動作,在沒有權限的情況下,應用程序無法去訪問本地文件系統,處理器,網絡等。例如,在遠程操作的情況下,代碼需要有簽名證書。
除了去解釋java字節碼,許多軟件實現的JVM都有一個JIT編譯器用于生成頻繁執行的方法機器代碼。機器代碼是可以直接被cpu解析執行的,所以比字節碼速度更快。
你無需去理解JVM的內部,就能編寫并運行一個JAVA應用程序。但是,如果你知道了其中的一些原理,就能避免一些性能上的問題。本文以sunspot為例子來說明。
架構
JVM主要有兩大子系統:
這里的內存是底層操作系統分配給JVM的,如下所示:

類加載器
JVM應用不同類型的類加載器構造了層次結構:
當類加載器收到去加載一個類的請求時,會去檢查cache中該類是否已經被加載,然后向其父加載器發出加載請求,如果其父加載器加載失敗,那么它就自己進行加載。一個子類加載器可以檢查其父類加載器的cache中是否加載了某個類,但是父類加載器無法查看子類cache中的緩存。這樣設計的原因是為了防止子類加載器加載那些已經被父類加載器加載過的類。(呼,好繞口。。。)
java文件經過編譯后會生成.class字節碼文件,它定義了JVM中的一個類型,包括域,方法,繼承信息,注解和其他元數據。我們知道,類是JVM能加載的最小程序代碼單元,將一個新的類加入到當前運行中的JVM中,首先要對類文件進行加載和連接,然后將一個代表該類的Class對象交給JVM,才可以創建新的實例。
加載與連接
JVM要執行.class文件中的字節碼,首先必須以字節流的方式將文件讀入,然后轉化為可以使用的格式加入到運行的JVM中。這兩步被稱為加載與連接。
加載
這個過程首先會創建一個字節數組,然后從文件系統中讀取構成類文件的字節流,最后產生與所加載類對應的Class對象。這個過程中會對類做一些基本檢查,加載結束后,Class對象還不完整,所以類是不可用的。
連接
加載工作完成后,類需要被連接起來,這里分為3個階段:
連接與加載的最終產物是一個Class對象,它可以表示加載并連接起來的新類型,可以用它來創建新實例。
執行引擎
執行引擎負責執行被加載進內存的字節碼指令,為了使計算機能夠識別字節碼,執行引擎采用了兩種方式:
盡管JIT的編譯過程比普通的解釋過程要耗時,但是它只需編譯一次,對于那些上千次調用的方法來說,直接執行機器代碼就比每次都要轉換字節碼再執行要劃算了。
JIT編譯器對于JVM而言并非是必須的組件,同時,也不是提升JVM性能的唯一手段。JVM規范只是定義了字節碼與機器代碼的對應關系,至于如何具體實現,就是不同版本JVM的事情了。
內存模型
JAVA內存模型是建立在內存自動管理機制之上的。當一個對象不在被應用程序引用,垃圾收集器GC就丟棄它并釋放內存。這與其他編程語言需要手動釋放對象的方式不同。
JVM從操作系統中申請來內存,并分割成如下幾個區域:
垃圾回收
內存自動管理是JAVA平臺最重要的組成部分。一個java進程既有棧又有堆,其中,棧保存了基本類型的局部變量,以及自定義類型變量在堆中存放的地址。堆中保存了要創建的對象。java對堆內存回收和再利用的基本算法被稱為標記和清除。
最簡單的標記和清除算法首先會暫停所有正在運行的線程,然后堆中遍歷引用樹,標記出“活”的對象,遍歷完成后則清除回收所有未被標記的對象。其中,“活”的對象是指在任意用戶線程的棧幀中存在引用的對象。被清除的內存并不會還給OS,而是交給JVM。
JAVA對標記清除算法做了改進,采用“分代式垃圾收集”方法,因為對象的生存期或者很短或者很長,所以根據對象的生命周期將堆內存劃分為不同區域,充分利用對象生命周期的特點。因此,同一個對象在其不同生命周期中,對它的引用可能指向了不同的內存區域。
將堆根據類實例的生存周期劃分為不同區域使得內存管理更加有效,GC無需遍歷整個堆。絕大多數對象的生命周期都很短,而那些略長一些的對象所占內存在程序結束之前不大可能被全部回收。
內存區域劃分
收集方式
對不同區域的內存回收方式是不同的,具體來講主要分為年輕代收集和完全收集?!?/p>
年輕代收集
我們將Eden區和Survivor Space稱為年輕代,對這部分內存的清理與收集的過程很簡單:
完全收集
當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方法)新開一個線程,其余的線程都來自與此線程,并獨立執行。多個線程可以并發地在不同處理器中執行,或者共享同一個處理器。
新聞熱點
疑難解答