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