本文將介紹Java虛擬機的基本結構,各組成部分的做用,以及相互之間是如何協調的。而要了解這些,首先必須瞭解Java堆、Java棧、永久區和元數據區的基本概念。java
類加載子系統負責從文件系統或者網絡中加載Class信息,加載的類信息放在一塊稱爲方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池的信息,包括字符串字面量和數字常量(這部分常量信息是class文件中常量池部分的內存映射)。算法
程序計數器(Program Counter Register)是一塊較小的內存空間,他能夠看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裏(僅是概念模型,各類虛擬機可能會經過一些更高效的方式去實現),字節碼解釋器工做時就說經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。網絡
因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。數據結構
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址:若是正在執行的是Native方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。多線程
Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。架構
局部變量表存放了編譯器可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,他不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。函數
在Java虛擬機規範中,對這個區域規定了兩種異常狀況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverFlowError異常;若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。性能
與虛擬機棧的做用類似,他們之間的區別是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。優化
Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。spa
Java堆是垃圾收集器管理的主要區域,所以不少時候被稱爲GC堆。因爲如今收集器基本都採用分代收集算法,因此Java堆中還能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。
與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
運行時常量池相對於Class文件常量池的另一個重要特徵是ju'bei具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用的比較多的即是String類的intern()方法。
直接內存並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也被頻繁的使用,並且也可能致使OutOfMemoryError異常出現。它直接在Java堆外、直接向系統申請的內存空間。一般,訪問直接內存的速度會優於Java堆。所以,在讀寫頻繁的場合可能會考慮使用直接內存。因爲直接內存在Java堆外,所以它的大小不會直接受限於Xmxd指定的最大堆大小,可是系統內存是有限的,Java堆和直接內存的總和依然受限於操做系統能給出的最大內存。
在JDK 1.4中新加入了NIO類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,他可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。
Java堆是和Java應用程序關係最爲密切的內存空間,幾乎全部的對象都存放在堆中。而且Java堆是徹底自動化管理的,經過垃圾回收機制,垃圾對象會被自動清理,而不須要顯式的釋放。
根據垃圾回收機制的不一樣,Java堆有可能擁有不一樣的結構,最多見的一種是將Java堆分爲新生代和老年代。其中,新生代存放新生對象或者年齡不大的對象,老年代存放老年對象。新生代可能分爲eden區、s0區、s1區,s0和s1也被稱爲from和to區域,他們是兩塊大小相等、能夠互換角色的內存空間。
在絕大多數狀況下,對象首先分配在eden區,在一次新生代回收(Young GC)後,若是對象還存活,則會進入s0或s1,以後,沒通過一次Young GC,對象若是存活,他的年齡就會加1.當對象的年齡達到必定條件後,就會被認爲是老年代,從而進入老年代。
Java棧是一塊線程私有的內存空間。若是說,Java堆和程序數據密切相關,那麼Java堆就是和線程執行密切相關的。線程執行的基本行爲是函數調用,每次函數調用的數據都是經過Java棧傳遞的。
Java棧與數據結構上的棧有着相似的含義,他是一塊先進後出的數據結構,只支持出棧和入棧兩種操做。Java虛擬機提供了參數-Xss來指定線程的最大棧空間,這個參數也直接決定了函數調用的最大深度。
局部變量表是棧幀的重要組成部分之一。它用於保存函數的參數以及局部變量。局部變量表中的變量只在當前函數調用中有效,當函數調用結束後,隨着函數棧幀的銷燬,局部變量表也會隨之銷燬。
因爲局部變量表在棧幀之中,所以,若是函數的參數和局部變量較多,會使得局部變量表膨脹,從而每一次函數調用就會佔用更多的棧空間,最終致使函數的嵌套調用次數減小。
局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象是不會被回收的。所以,理解局部變量表對理解垃圾回收也有必定的幫助。
可使用參數-XX:+PrintGC,在輸出的日誌中,能夠看到垃圾回收先後堆的大小。
它主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間。
大部分Java字節碼指令須要進行常量池訪問,在幀數據區中保存着訪問變量池的指針,方便程序訪問常量池。同時異常處理表也是幀數據區中重要的一部分。
棧上分配是Java虛擬機提供的一項優化技術,他的基本思想是,對於那些線程私有的對象(這裏指不可能被其餘線程訪問的對象),能夠將他們打散分配在棧上,而不是分配在堆上。分配在棧上的好處是能夠在函數調用結束後自行銷燬,而不須要垃圾回收器的介入,從而提升系統的性能。
棧上分配的一個技術基礎是進行逃逸分析。逃逸分析的目的是判斷對象的做用域是否有可能逃逸出函數體。
對於大量的零散小對象,棧上分配提供了一種很好的對象分配優化策略,棧上分配速度快,而且能夠有效避免垃圾回收帶來的負面影響,但因爲和堆空間相比,棧空間較小,所以對於大對象也不適合在棧上分配。
方法區是一塊全部線程共享的內存區域,用於保存系統的類信息,好比類的字段、方法、常量池等。方法去的大小決定了系統能夠保存多少類,若是系統定義了太多的類,致使方法區溢出,虛擬機一樣會拋出內存溢出錯誤。
在JDK 1.六、JDK 1.7中,方法區能夠理解爲永久區(Perm)。永久區能夠用參數-XX:PermSize和-XX:MaxPermSize指定,默認狀況下,-XX:MaxPermSize爲64M。一個大的永久區能夠保存更多的類信息。若是系統使用了一些動態代理,那麼有可能會在運行時產生大量的類,若是這樣,就須要設置一個合理的永久區大小,確保不發生永久區內存溢出。
在JDK 1.8中,永久區已經被完全移除。取而代之的是元數據區,元數據區大小能夠用參數-XX:MaxMetaspaceSize指定(一個大的元數據區可使系統支持更多的類),這是一塊堆外的直接內存。與永久區不一樣,若是不指定大小,默認狀況下,虛擬機會耗盡全部的可用系統內存。