Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若干個不同的數據區域。這些區域的用途各不相同,同時也依據著各自的執行規則,獨立的創建和銷毀數據。
虛擬機內存的劃分,如圖所示:
線程之間互相獨立的區域有:
虛擬機棧 、本地方法棧、程序計數器
線程可以共享數據的區域:
方法區 、堆
每個區域的作用分別如下:
程序計數器 Program Counter Register:
眾所周知,虛擬機處理多線程時,是通過輪流的切換線程,來獲取cpu的執行機會的。在虛擬機執行程序的過程中,當線程執行到某一位置時,虛擬機將cpu的執行機會出讓給了其他線程,此時原有線程的執行(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )位置需要被記錄下來,而新得到執行機會的線程,又需要提供上次執行的位置,以此來保證程序中的多個線程可以持續的並行的執行下去。
程序計數器的作用就是將各個線程下次所執行的(字節碼)行號(准確來說是指令的地址)記錄下來,以保證其下次執行時可以正確的執行。
根據程序計數器的作用,我們可以知道:
1、每個線程都在這個區域中都應該擁有一個只為自己提供服務的程序計數器,它們之間是獨立存儲,互不影響的存在。
2、我們還可以知道,程序計數器只記錄字節碼的行號,因此當線程執行本地方法(Native method)時,計數器的值是空。
3、程序計數器所耗費的內存空間非常小,因此這個區域是不會拋出OutOfMemoryError錯誤的。
虛擬機棧 VM Stack:
線程想要正常的運行下去,單靠程序計數器來記錄行號是遠遠不止的。線程還需要擁有自己的運行空間,在這個空間中,虛擬機可以保存方法的執行順序、方法的內部局部變量,方法在運算時,所需要的內存空間等。
在數據結構中,棧的特性最滿足方法的進入返回的結構的。而這塊區域的主(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )要作用就是線程在執行java方法時所需要記錄的數據。因此我們將這塊區域稱之為虛擬機棧。但是要記住這裡與我們在工作中通常指的棧並不等同,這個我會在後邊介紹。
虛擬機棧的結構如下:
而對於每一個棧幀內部的劃分又是這樣的:
每一部分的作用如下:
(1)局部變量表:每一個方法都可以定義一個只屬於自己的局部變量,當這個方法運行結束後,這個局部變量的生命周期也就宣告結束。所以每一個方法都應該擁有一個塊屬於自己的內存區域用來保存方法內部定義的局部變量。這塊區域就是局部變量表,我們平常工作中所指的棧,實際上指的是虛擬機棧中的棧幀中的局部變量表。
(2)操作數棧:每個方法的內部都可以計算數據,而計算數據勢必需要擁有一塊內存區域,為虛擬機用來進行數值計算。因此在棧幀中,就需要有一塊區域專門為當前方法計算數據使用,它就是操作數棧。
在每進行一次完整的計算之後,棧中的數據都已經出棧,所以操作數棧的空間在一個方法內部是可以反復使用的。所以虛擬機在分配內存大小時,只分配當前方法,單次完整計算所需要的最大內存空間給當前棧幀,以減少內存的消耗。
同時為了增加運行效率,減少數據的不斷復制,在大部分虛擬機的實現中,將當前方法的局部變量表和上層方法的操作數棧的內存形成部分重疊,從而減少參數的不斷復制而引起的性能消費。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )
(3)動態連接:
虛擬機在執行方法時有兩種形式被用來確定執行指令所對應的方法,
第一種是類加載時,可以直接確定要執行的方法,譬如靜態方法,私有方法,final方法等。這種形式叫做靜態解析。
第二種是在真正運行時,根據對象的真實引用來判斷當前真正要執行的方法。這種形式稱之為動態連接。
在字節碼文件中,都存在一個常量池,在這個常量池中保存有大量的符號引用,這個符號引用是每一個方法的間接引用。在字節碼指令的中,使用的是這個符號引用。但是在運行時階段,肯定需要調用到要執行方法在內存中真實的地址。這就需要將間接引用轉化成直接引用。而這裡的“動態連接”就是為了保證在運行時階段,方法可以正確的找到要調用的方法,每個棧幀將自己在運行時常量池中所對應的真實地址記錄的位置。
這裡需要注意的是,在棧幀中的動態連接和查找符號引用為真實引用中的動態連接,是兩個概念。前者表示的是一個區域,後者表示的是一種查找方式。
(4)返回地址:
退出當前方法的方式有兩種,第一種是遇到返回指令時,正常的退出當前方法。另一種形式是遇到沒有捕獲而被拋出的異常。無論何種返回形式,在方法退出後,棧幀的頂端都應是當前退出方法的上層方法。同時上層方法的執行狀態也需要根據當前的返回結果重新調整。所以每個棧幀可以利用“返回地址”這塊區域幫助上層方法恢復狀態。
(5)附加信息:對於虛擬機規范中沒有申明的,擁有指定存放位置的信息可以由各個虛擬機自己決定,放置到這個區域中。
本地方法棧 Native Stack
在虛擬機中,不但運行java方法,還會運行本地方法,也就是常見的Native 關鍵字修飾的方法。在虛擬機棧中,會為每個線程獨立的開辟一個專門運行java語言(更准確的說應該是字節碼)的方法(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )棧,但是對於本地方法,則是使用另外的一塊內存區域來保存線程的調用狀態,這塊區域就是本地方法棧。他的作用跟虛擬機棧基本相似,其區別就是一個為java方法服務,一個為Native發光法服務。在虛擬機規范中,對於本地方法棧中的結構、方法的語言、方式,都沒有強制規定,各個虛擬機可以自由的實現它。
Java堆 Java Heap
我們平常所說的,在堆中創建一個實例,指的就是這個堆。這是虛擬機所管理的內存中最大的一塊。在虛擬機中,幾乎所有的實例以及數組所分配的內存空間都會被放置在這個堆中。
由於java堆是對象實例的的主要存放位置,因此虛擬機的垃圾回收機制的主要工作區域。
根據Java的內存回收機制,我們可以將堆的大小和內容劃分成如下的形式:
根據java堆的特性,我們也可以知道,這塊區域是一塊線程共享的區域。同時我們也可以看出來,這塊區域,所可以使用在物理上非連續的內存,只要在邏輯上保持連續即可。
方法區 Method Area
方法區的主要作用是保存類信息、常量、靜態變量以及即時編譯後的代碼等數據。這個區域中的數據仍然會被GC的代回收所涉及到。我們平常所說的永久代,指的就是這個區域。
盡管這個區域也被稱之為永久代,但是當數據進入這個區域中,仍然可能會被回收。這個區域的回收目標主要是常量池的回收,以及類型的卸載。
運行時常量池 Runtime Constant Pool
這塊區域屬於方法區的中的一塊子區域。
在Class文件中,除了有類版本、字段、方法、接口等,還有一個信息區(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )域是常量池。常量池中的數據將會在類被加載後,存在到運行時常量池中。
而類文件中的常量池主要包括各種字面量和符號引用。符號引用在講解棧幀時,有所涉及。
字面量可以理解為java語言中的常量,如字符串、final修飾的變量等。
符號引用則是指以下三種固定信息:
(1)類和接口的全限定名稱
(2)字段的名稱和描述符
(3)方法的名稱和描述符
java語言在編譯成Class文件後,並沒有關於方法和字段在內存中最終布局的信息。所以當虛擬機使用這些變量或方法時,需要先從常量池中,找到這些數據對應的符號引用,然後在方法的棧幀中的動態連接區域中找到其對應的內存真實位置。
在日常工作中,我們經常會遇到兩種內存溢出的錯誤:
1、OutOfMemoryError
2、StackOverflowError
OutOfMemoryError指的是一個區域中,由於數據的不斷增加,導致區域無法再從物理內存總申請到更大的空間,或者是區域所申請的空間已經到達虛擬機運行參數所給該區域設定的最大值,那麼就會拋出這個錯誤。
StackOverflowError則指的是內存中的棧結構在不斷的入棧,最終導致棧的深度超過了虛擬機所允許的棧深度時,所拋出的錯誤。