首先經過一張圖瞭解 Java程序的執行流程:html
咱們編寫好的Java源代碼程序,經過Java編譯器javac編譯成Java虛擬機識別的class文件(字節碼文件),而後由 JVM 中的類加載器加載編譯生成的字節碼文件,加載完畢以後再由 JVM 執行引擎去執行。在加載完畢到執行過程當中,JVM會將程序執行時用到的數據和相關信息存儲在運行時數據區(Runtime Data Area),這塊區域也就是咱們常說的JVM內存結構,垃圾回收也是做用在該區域。java
關於這幅圖涉及到的:算法
①、class文件數組
②、類加載器數據結構
③、運行時數據區多線程
④、執行引擎併發
⑤、垃圾回收器oracle
這都是接下來將要介紹的重點。jvm
本篇博客咱們將首先介紹什麼是運行時數據區。函數
PS:下面介紹的是根據 Java虛擬機規範 定義的運行時數據區,上一篇博客咱們講過根據虛擬機規範實現的虛擬機有不少個,而不一樣的虛擬機其運行時數據區定義也會有所不一樣。好比默認的 HotSpot 在實現 JDK1.7 虛擬機規範時,其常量池的定義不在方法區中,而是移到了堆中;到了 HotSpot JDK1.8 中,則完全移除了持久代(方法區)而使用Metaspace(元數據區)來進行替代等等,關於這些區別本篇博客也會在文章末尾進行相應的說明。
①、Java虛擬機規範定義的運行時數據區
②、HotSpot JDK1.8定義的運行時數據區
注意:HotSpot實現的運行時數據區和Java虛擬機規範定義的仍是有所不一樣的,
①、將Java虛擬機棧和本地方法棧合二爲一;
②、元數據區取代了方法區,而且元數據區不在Java虛擬機中,而是在本地內存中。
③、運行時常量池由方法區中移到了堆中
程序計數器(Program Conputer Register)這是一塊較小的內存空間,能夠看作是當前線程所執行的字節碼的行號指示器,在虛擬機的概念模型裏,字節碼解釋器的工做就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
①、線程私有
Java虛擬機支持多線程,是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任一肯定的時刻,一個處理器只會執行一條線程中的指令,所以爲了線程切換後能恢復到正確的執行位置,每條線程都須要一個獨立的程序計數器。所以線程啓動時,JVM 會爲每一個線程分配一個PC寄存器(Program Conter,也稱程序計數器)。
②、記錄當前字節碼指令執行地址
若是當前線程執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native 方法,則這個計數器值爲空(Undefined)。
③、不拋 OutOfMemoryError 異常
程序計數器的空間大小不會隨着程序執行而改變,始終只是保存一個 returnAdress 類型的數據或者一個與平臺相關的本地指針的值。因此該區域是Java運行時內存區域中惟一一個Java虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。
Java虛擬機棧(Java Virtual Machine stack),這塊區域也是線程私有的,與線程同時建立,用於存儲棧幀。Java 每一個方法執行的時候都會同時建立一個棧幀(Stack Frame)用於存儲局部變量表、操做棧、動態連接、方法出口等信息,每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
①、線程私有
隨線程建立而建立,聲明週期和線程保持一致。
②、由棧幀組成
線程每一個方法被執行的時候都會建立一個棧幀,用於存儲局部變量表、操做棧、動態連接、方法出口等信息,每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
③、拋出 StackOverflowError 和 OutOfMemoryError 異常
若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError 異常;若是虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存時會拋出 OutOfMemoryError 異常。
本地方法棧(Native Method Stacks)做用和虛擬機棧類型,虛擬機棧執行的是Java方法,本地方法棧執行的是 Native 方法,本地方法棧也會拋出拋出 StackOverflowError 和 OutOfMemoryError 異常。
注意:因爲虛擬機規範並無對本地方法棧中的方法使用語言、使用方式和數據結構強制規定,所以具體的虛擬機能夠自由實現它。上圖咱們也給出在 HotSpot 虛擬機中,本地方法棧和虛擬機棧合爲一體了。
Java堆是Java虛擬機所管理內存最大、被全部線程共享的一塊區域,目的是用來存放對象,基本上全部的對象實例和數組都在堆上分配(不是絕對)。Java堆也是垃圾回收器管理的主要區域。
①、線程共享
堆存放的對象,某個線程修改了對象屬性,另一個線程從堆中獲取的該對象是修改後的對象,爲何堆要設計成線程共享呢?
咱們能夠假設堆是線程私有的,很顯然一個系統建立的對象會有不少,並且有些對象會比較大,若是設計成線程私有的,那麼若是有不少線程同時工做,那麼都必須給他們分配相應的私有內存,我相信內存很快就撐爆了,很顯然將堆設計爲線程共享是最好不過了,不過凡事都具備兩面性,線程共享的設計這也帶來了多線程併發資源衝突問題,關於這個問題因爲不是本系列博客的主旨,這裏就不作詳細介紹了。
②、存放對象
基本上全部的對象實例和數組都要在堆上進行分配,可是隨着 JIT 編譯器的發展和逃逸分析技術的成熟,棧上分配、標量替換等優化技術會致使對象不必定在堆上進行分配。
③、垃圾收集
Java堆也被稱爲「GC堆」,是垃圾回收器的主要操做內存區域。當前垃圾回收器都是使用的分代收集算法,因此Java堆還能夠分爲:新生代和老年代,而新生代又能夠分爲 Eden 空間、From Survivor 空間、To Survivor空間。這是爲了更好的回收內存,關於垃圾回收算法在後續博客會詳細介紹。
④、拋出 OutOfMemoryError 異常
根據Java虛擬機規範,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上連續便可,實現時既能夠實現成固定大小,也能夠是擴展的。若是在堆中沒有完成實例分配,而且堆也沒法擴展,將拋出OutOfMemoryError 異常。
方法區(Method Area)用來存儲已被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
方法區也稱爲「永久代」,這是由於垃圾回收器對方法區的垃圾回收比較少,主要是針對常量池的回收以及對類型的卸載,回收條件比較苛刻。常常會致使對此內存未徹底回收而致使內存泄露,最後當方法區沒法知足內存分配時,將拋出 OutOfMemoryError 異常。
PS:在Java虛擬機規範中把方法區描述爲堆的一個邏輯部分(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4),在不少虛擬機中(JRockit、IBM J9等虛擬機不存在永久代的概念)。
在JDK1.8 的 HotSpot 虛擬機中,已經去掉了方法區的概念,用 Metaspace 代替,而且將其移到了本地內存來規劃了。
在Java虛擬機規範中,運行時常量池(Runtime Constant Pool)用於存放編譯期生成的各類字面量和符號引用,是方法區的一部分。可是Java虛擬機規範對其沒有作任何細節的要求,因此不一樣虛擬機實現商能夠按照本身的需求來實現該區域,好比在 HotSpot 虛擬機實現中,就將運行時常量池移到了堆中。
①、存放字面量、符號引用、直接引用
一般來講,該區域除了保存Class文件中描述的引用外,還會把翻譯出來的直接引用也存儲在運行時常量池,而且Java語言並不要求常量必定只能在編譯器產生,運行期間也可能將常量放入池中,好比String類的intern()方法,當調用intern方法時,若是池中已經包含一個與該String
肯定的字符串相同equals(Object)
的字符串,則返回該字符串。不然,將此String
對象添加到池中,並返回此對象的引用。關於該方法的介紹能夠看我這篇博客。
②、拋出 OutOfMemoryError 異常
運行時常量池是方法區的一部分,會受到方法區內存的限制,當常量池沒法申請到內存時,會拋出該異常。
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,它也不是Java虛擬機規範定義的內存區域。咱們能夠看到在 HotSpot 中,就將方法區移除了,用元數據區來代替,而且將元數據區從虛擬機運行時數據區移除了,轉到了本地內存中,也就是說這塊區域是受本機物理內存的限制,當申請的內存超過了本機物理內存,纔會拋出 OutOfMemoryError 異常。
直接內存也是受本機物理內存的限制,在JDK1.4中新加入的 NIO(new input/output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在Java堆裏面的 DirectByteBuffer 對象做爲這塊內存的引用操做,這樣避免了在Java堆和Native堆中來回複製數據,顯著提升性能。