Java內存模型
Java內存模型,就是Java程序運行時的內存模型。而Java代碼是在Java虛擬機上運行的,由Java虛擬機解釋執行(解釋器)或者編譯執行(即使編譯器,JIT)來完成。所以Java內存模型也就是Java虛擬機的運行時內存模型。
Java解釋器,可以在任何移植了解釋器的機器上執行Java字節碼(.class)。即使編譯器,Java程序運行時動態地將字節碼翻譯成特定CPU的機器碼。而編譯指的是.java文件javac成.class文件。
Java內存模型結構
C/C++程序猿需要時刻注意內存的釋放,而Java全權交給虛擬機來處理內存。虛擬機有垃圾回收(GC),會去自動進行垃圾回收。那么都是自動的,好學什么?其實虛擬機自動回收垃圾有一套自己的規律,并不能根據程序猿編寫的代碼隨時GC,所以只能根據垃圾回收的規律,合理安排內存,這就要求我們必須徹底了解JVM的內存管理機制。
運行時內存模型,分為線程私有和共享數據區兩大類。
線程私有
線程私有的數據區包含程序計數器、虛擬機棧、本地方法區。
程序計數器
作用:當JVM解釋字節碼文件時,存儲當前線程所執行的字節碼行號。所以每一個線程都有一個程序計數器。
Java程序執行時,是依靠Java虛擬機的解釋器通過改變當前線程程序計數器的值來按照流程執行指令。當然還有一個好處體現在多線程方面。一個內核同一時間只能執行一條指令,多線程的工作是依靠線程輪流切換(占用CPU時間)來達到的。對于每一個線程來說,必須有一個地方存儲中斷時的地址(行號),才能保證切回來時回到正確地址。Java規范沒有規定該內存區域OOM(OutOfMemoryError)異常。
注意,上面說的指令指的是Java方法,執行native方法時,程序計數器為空。
Java虛擬機棧
虛擬機棧也是線程私有的,其生命周期和線程一樣。
虛擬機棧描述的是Java方法執行的內存模型:每個方法(不包括native方法)在執行時都會創建一個棧幀,用于存儲局部變量,操作數棧,動態鏈接,方法出口等信息。每一個方法從調用到執行結束的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
在Java虛擬機規范中,對該區域內存規定了兩種異常狀況:
- StackOverFlowError:當線程請求棧深度超出虛擬機棧所允許的深度時拋出
- OutOfMemoryError:當Java虛擬機動態擴展到無法申請足夠內存時拋出
棧幀
棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區的虛擬機棧(Virtual Machine Stack)的棧元素。
在編譯代碼的時候,棧幀中需要多大的局部變量表,多深的操作數棧都已經完全確定了,并且寫入到了方法表的Code屬性中,因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決于具體虛擬機的實現。
對于執行引擎來講,活動線程中,只有虛擬機棧頂的棧幀才是有效的,稱為當前棧幀(Current Stack Frame),這個棧幀所關聯的方法稱為當前方法(Current Method)。執行引用所運行的所有字節碼指令都只針對當前棧幀進行操作。
棧幀包括以下內容:
- 局部變量表,是一組變量值存儲空間,用于存放方法參數和方法內部定義的局部變量。其大小以slot為最小單位(32位)。在Java程序編譯為Class文件時,就在方法表的Code屬性的max_locals數據項中確定了該方法需要分配的最大局部變量表的容量。它存放了編譯期可知的各種基本數據類型(boolean,byte,char,short,int,float,long,double),對象引用,以及returnAddress類型(指向了一條字節碼指令地址)
- 操作數棧,操作數棧也常被稱為操作棧,它是一個后入先出棧。同局部變量表一樣,操作數棧的最大深度也是編譯的時候被寫入到方法表的Code屬性的max_stacks數據項中。
- 動態鏈接,每個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態連接。
- 方法返回地址,當一個方法被執行后,有兩種方式退出。
- 執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的的方法稱為調用者)。是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法方式稱為正常完成出口(Normal Method Invocation Completion)。
- 在方法執行過程中遇到了異常,并且這個異常沒有在方法體內得到處理。只要該異常在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的調用都產生任何返回值的。
- 附加信息,虛擬機規范允許具體的虛擬機實現增加一些規范里沒有描述的信息到棧幀中,例如與高度相關的信息,這部分信息完全取決于具體的虛擬機實現。在實際開發中,一般會把動態連接,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息。
執行引擎,Java虛擬機的執行引擎在執行代碼時,都有解釋執行(通過解釋器執行)和編譯執行(通過JIT產生本地代碼(特定CPU機器碼)執行)兩種選擇。
以上內容參考自深入理解Java虛擬機筆記---運行時棧幀結構,只是做到了了解棧幀的結構。
日常開發中,經常把內存模型分為堆和棧,而其中的棧其實就是虛擬機棧,更準確的說是虛擬機棧中棧幀的局部變量表。
本地方法棧
本地方法棧則為虛擬機使用到的Native方法提供內存空間。有些虛擬機的實現直接把本地方法棧和虛擬機棧合二為一,比如非常典型的Sun HotSpot虛擬機。
和虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
共享數據區
所有線程共享的數據區包含Java堆、方法區,在方法區內有一個常量池。
堆內存
Java堆是Java虛擬機所管理的內存中最大的一塊。在虛擬機啟動時創建。此內存唯一的目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存(隨著JIT的發展,有一部分對象實例會放在棧內存)。
Java堆事垃圾收集器管理的主要區域,又稱其為"GC堆"(Garbage Collection Heap)。
- 從內存回收的角度來看,Java堆可以細分為新生代和老年代(舊生代)。
- 從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區。
進一步劃分的目的是為了更好的回收內存。
在32位系統上最大為2G,64位系統上無限制。可通過-Xms和-Xmx控制,-Xms為JVM啟動時申請的最小Heap內存,-Xmx為JVM可申請的最大Heap內存。
如果堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError異常。
方法區
它主要存放一杯虛擬機加載的類信息,常量,靜態變量,即使編譯器后的代碼等數據。根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
運行時常量池
運行時常量區是方法區的一部分。用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。還會有一些符號引用轉換的直接引用一保存在運行時常量池中。
運行時常量池具備動態性,也就是運行期間也可以將新的常量放入池中,例如String.intern()方法。
當常量池無法再申請到內存時,會拋出OutOfMemoryError異常。
直接內存
上面描述的內存模型是運行時的數據區。而直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域。這部分區域也會導致OutOfMemoryError異常出現。
在JDK1.4中新加入類NIO類,引入了一種基于通道與緩沖區的I/O方式,它可以使用Native函數庫直接分配堆外內存。它可以使用Native函數庫直接分配堆外內存。
由于它是本機內存(Ram),肯定會受到本機總內存大小以及處理器尋址空間的限制。所以在動態擴展時會出現OOM異常。
雜記
變量的初始化
變量根據作用域分有局部變量和類變量。局部變量不像類變量那樣存在“準備階段”。類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始值;另外一次在初始化階段,賦予程序員定義的值。因此即使在初始化階段程序員沒有為類變量賦值也沒有關系,類變量仍然具有一個確定的初始值。但局部變量就不一樣了,如果一個局部變量定義了但沒有賦初始值是不能使用的。
字面量,常量,直接量,符號引用類型
- 字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明為final的常量值等。還包含了基本數據類型的直接量,以及引用類型的空指向。
- 符號引用:編譯語言層面的概念,包括以下3類:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
直接量,指在程序中直接出線的常量值。
遺留問題
只有程序計數器內存區域不會拋出OOM異常,其它能夠拋出OOM異常的內存區域,都有動態擴展這一特性。