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