JVM內存模型
Java虛擬機(Java Virtual Machine=JVM)的內存空間分為五個部分,分別是:
程序計數器
Java虛擬機棧
本地方法棧
堆
方法區。
下面對這五個區域展開深入的介紹。程序計數器
1.1. 什么是程序計數器?
程序計數器是一塊較小的內存空間,可以把它看作當前線程正在執行的字節碼的行號指示器。也就是說,程序計數器里面記錄的是當前線程正在執行的那一條字節碼指令的地址。
注:但是,如果當前線程正在執行的是一個本地方法,那么此時程序計數器為空。
1.2. 程序計數器的作用
程序計數器有兩個作用:
字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
在多線程的情況下,程序計數器用于記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。
1.3. 程序計數器的特點
是一塊較小的存儲空間
線程私有。每條線程都有一個程序計數器。
是唯一一個不會出現OutOfMemoryError的內存區域。
生命周期隨著線程的創建而創建,隨著線程的結束而死亡。
- Java虛擬機棧(JVM Stack)
2.1. 什么是Java虛擬機棧?
Java虛擬機棧是描述Java方法運行過程的內存模型。
Java虛擬機棧會為每一個即將運行的Java方法創建一塊叫做“棧幀”的區域,這塊區域用于存儲該方法在運行過程中所需要的一些信息,這些信息包括:
局部變量表
存放基本數據類型變量、引用類型的變量、returnAddress類型的變量。
操作數棧
動態鏈接
方法出口信息
等
當一個方法即將被運行時,Java虛擬機棧首先會在Java虛擬機棧中為該方法創建一塊“棧幀”,棧幀中包含局部變量表、操作數棧、動態鏈接、方法出口信息等。當方法在運行過程中需要創建局部變量時,就將局部變量的值存入棧幀的局部變量表中。
當這個方法執行完畢后,這個方法所對應的棧幀將會出棧,并釋放內存空間。
注意:人們常說,Java的內存空間分為“棧”和“堆”,棧中存放局部變量,堆中存放對象。
這句話不完全正確!這里的“堆”可以這么理解,但這里的“棧”只代表了Java虛擬機棧中的局部變量表部分。真正的Java虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。
2.2. Java虛擬機棧的特點
局部變量表的創建是在方法被執行的時候,隨著棧幀的創建而創建。而且,局部變量表的大小在編譯時期就確定下來了,在創建的時候只需分配事先規定好的大小即可。此外,在方法運行的過程中局部變量表的大小是不會發生改變的。
Java虛擬機棧會出現兩種異常:StackOverFlowError和OutOfMemoryError。
a) StackOverFlowError:
若Java虛擬機棧的內存大小不允許動態擴展,那么當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。
b) OutOfMemoryError:
若Java虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出OutOfMemoryError異常。
Java虛擬機棧也是線程私有的,每個線程都有各自的Java虛擬機棧,而且隨著線程的創建而創建,隨著線程的死亡而死亡。
注:StackOverFlowError和OutOfMemoryError的異同?
StackOverFlowError表示當前線程申請的棧超過了事先定好的棧的最大深度,但內存空間可能還有很多。
而OutOfMemoryError是指當線程申請棧時發現棧已經滿了,而且內存也全都用光了。
- 本地方法棧
3.1. 什么是本地方法棧?
本地方法棧和Java虛擬機棧實現的功能類似,只不過本地方法區是本地方法運行的內存模型。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。
方法執行完畢后相應的棧幀也會出棧并釋放內存空間。
也會拋出StackOverFlowError和OutOfMemoryError異常。 - 堆
4.1. 什么是堆?
堆是用來存放對象的內存空間。
幾乎所有的對象都存儲在堆中。
4.2. 堆的特點
線程共享
整個Java虛擬機只有一個堆,所有的線程都訪問同一個堆。而程序計數器、Java虛擬機棧、本地方法棧都是一個線程對應一個的。
在虛擬機啟動時創建
垃圾回收的主要場所。
可以進一步細分為:新生代、老年代。
新生代又可被分為:Eden、From Survior、To Survior。
不同的區域存放具有不同生命周期的對象。這樣可以根據不同的區域使用不同的垃圾回收算法,從而更具有針對性,從而更高效。
堆的大小既可以固定也可以擴展,但主流的虛擬機堆的大小是可擴展的,因此當線程請求分配內存,但堆已滿,且內存已滿無法再擴展時,就拋出OutOfMemoryError。
- 方法區
5.1. 什么是方法區?
Java虛擬機規范中定義方法區是堆的一個邏輯部分。
方法區中存放已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等。
5.2. 方法區的特點
線程共享
方法區是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區。
永久代
方法區中的信息一般需要長期存在,而且它又是堆的邏輯分區,因此用堆的劃分方法,我們把方法區稱為老年代。
內存回收效率低
方法區中的信息一般需要長期存在,回收一遍內存之后可能只有少量信息無效。
對方法區的內存回收的主要目標是:對常量池的回收 和 對類型的卸載。
Java虛擬機規范對方法區的要求比較寬松。
和堆一樣,允許固定大小,也允許可擴展的大小,還允許不實現垃圾回收。
5.3. 什么是運行時常量池?
方法區中存放三種數據:類信息、常量、靜態變量、即時編譯器編譯后的代碼。其中常量存儲在運行時常量池中。
我們一般在一個類中通過public static final來聲明一個常量。這個類被編譯后便生成Class文件,這個類的所有信息都存儲在這個class文件中。
當這個類被Java虛擬機加載后,class文件中的常量就存放在方法區的運行時常量池中。而且在運行期間,可以向常量池中添加新的常量。如:String類的intern()方法就能在運行期間向常量池中添加字符串常量。
當運行時常量池中的某些常量沒有被對象引用,同時也沒有被變量引用,那么就需要垃圾收集器回收。
- 直接內存
直接內存是除Java虛擬機之外的內存,但也有可能被Java使用。
在NIO中引入了一種基于通道和緩沖的IO方式。它可以通過調用本地方法直接分配Java虛擬機之外的內存,然后通過一個存儲在Java堆中的DirectByteBuffer對象直接操作該內存,而無需先將外面內存中的數據復制到堆中再操作,從而提升了數據操作的效率。
直接內存的大小不受Java虛擬機控制,但既然是內存,當內存不足時就會拋出OOM異常。
綜上所述
Java虛擬機的內存模型中一共有兩個“棧”,分別是:Java虛擬機棧和本地方法棧。
兩個“棧”的功能類似,都是方法運行過程的內存模型。并且兩個“棧”內部構造相同,都是線程私有。
只不過Java虛擬機棧描述的是Java方法運行過程的內存模型,而本地方法棧是描述Java本地方法運行過程的內存模型。
Java虛擬機的內存模型中一共有兩個“堆”,一個是原本的堆,一個是方法區。方法區本質上是屬于堆的一個邏輯部分。堆中存放對象,方法區中存放類信息、常量、靜態變量、即時編譯器編譯的代碼。
堆是Java虛擬機中最大的一塊內存區域,也是垃圾收集器主要的工作區域。
程序計數器、Java虛擬機棧、本地方法棧是線程私有的,即每個線程都擁有各自的程序計數器、Java虛擬機棧、本地方法區。并且他們的生命周期和所屬的線程一樣。
而堆、方法區是線程共享的,在Java虛擬機中只有一個堆、一個方法棧。并在JVM啟動的時候就創建,JVM停止才銷毀。
JVM內存模型總體架構圖
Run-Time Data Areas
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.
JVM定義了不同運行時數據區,他們是用來執行應用程序的。某些區域隨著JVM啟動及銷毀,另外一些區域的數據是線程性獨立的,隨著線程創建和銷毀。
程序計數器(The Program Counter Register)多線程時,當線程數超過CPU數量或CPU內核數量,線程之間就要根據時間片輪詢搶奪CPU時間資源。因此每個線程有要有一個獨立的程序計數器,記錄下一條要運行的指令。線程私有的內存區域。如果執行的是JAVA方法,計數器記錄正在執行的java字節碼地址,如果執行的是native方法,則計數器為空。
If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine's pc register is undefined. The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform.虛擬機棧 (Java Virtual Machine Stacks)線程私有的,與線程在同一時間創建。管理JAVA方法執行的內存模型。每個方法執行時都會創建一個楨棧來存儲方法的私有變量、操作數棧、動態鏈接方法、返回值、返回地址等信息。棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss參數可以設置虛擬機棧大小)。棧的大小可以是固定的,或者是動態擴展的。如果棧的深度是固定的,請求的棧深度大于最大可用深度,則拋出stackOverflowError;如果棧是可動態擴展的,但沒有內存空間支持擴展,則拋出OutofMemoryError。
The following exceptional conditions are associated with Java Virtual Machine stacks:If the computation in a thread requires a larger Java Virtual Machine stack than is permitted, the Java Virtual Machine throws aStackOverflowError.If Java Virtual Machine stacks can be dynamically expanded, and expansion is attempted but insufficient memory can be made available to effect the expansion, or if insufficient memory can be made available to create the initial Java Virtual Machine stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.
放在棧中的運算是比java堆速度快,所以盡量使用方法內的局部變量運算速度會比較快。使用jclasslib工具可以查看class類文件的結構。下圖為棧幀結構圖:
本地方法區(Native Method Stacks)和虛擬機棧功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C實現的。
JAVA堆(JVM heap)線程共享的,存放所有對象實例和數組。垃圾回收的主要區域。可以分為新生代和老年代(tenured)。新生代用于存放剛創建的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。新生代又可進一步細分為eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。剛創建的對象都放入eden,s0和s1都至少經過一次GC并幸存。如果幸存對象經過一定時間仍存在,則進入老年代(tenured)。
A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the heap, as well as, if the heap can be dynamically expanded or contracted, control over the maximum and minimum heap size.The following exceptional condition is associated with the heap:If a computation requires more heap than can be made available by the automatic storage management system, the Java Virtual Machine throws an OutOfMemoryError.
垃圾回收算法
標記-清除算法(Mark-Sweep)從根節點開始標記所有可達對象,其余沒標記的即為垃圾對象,執行清除。但回收后的空間是不連續的。復制算法(copying)將內存分成兩塊,每次只使用其中一塊,垃圾回收時,將標記的對象拷貝到另外一塊中,然后完全清除原來使用的那塊內存。復制后的空間是連續的。復制算法適用于新生代,因為垃圾對象多于存活對象,復制算法更高效。在新生代串行垃圾回收算法中,將eden中標記存活的對象拷貝未使用的s1中,s0中的年輕對象也進入s1,如果s1空間已滿,則進入老年代;這樣交替使用s0和s1。這種改進的復制算法,既保證了空間的連續性,有避免了大量的內存空間浪費。
標記-壓縮算法(Mark-compact)適合用于老年代的算法(存活對象多于垃圾對象)。標記后不復制,而是將存活對象壓縮到內存的一端,然后清理邊界外的所有對象。
-XX:+PrintGCDetails 打印垃圾回收信息
-Xms 為Heap區域的初始值,線上環境需要與-Xmx設置為一致,否則capacity的值會來回飄動-Xmx 為Heap區域的最大值-Xss(或-ss) 線程棧大小(指一個線程的native空間)1.5以后是1M的默認大小-XX:PermSize與-XX:MaxPermSize 方法區(永久代)的初始大小和最大值(但不是本地方法區)-XX:NewRatio 老年代與新生代比率-XX:SurvivorRatio Eden與Survivor的占用比例。例如8表示,一個survivor區占用 1/8 的Eden內存,即1/10的新生代內存,為什么不是1/9?因為我們的新生代有2個survivor,即S1和S22。所以survivor總共是占用新生代內存的 2/10,Eden與新生代的占比則為 8/10。-XX:MaxHeapFreeRatio GC后,如果發現空閑堆內存占到整個預估的比例小于這個值,則減小堆空間。-XX:MinHeapFreeRatio GC后,如果發現空閑堆內存占到整個預估的比例大于這個值,則增大堆空間。-XX:NewSize 新生代大小
參考文章:
http://www.cubrid.org/blog/dev-platform/understanding-java-garbage-collection/
http://www.Oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html
http://colobu.com/2015/04/07/minor-gc-vs-major-gc-vs-full-gc/
JVM定義了若干個程序執行期間使用的數據區域。這個區域里的一些數據在JVM啟動的時候創建,在JVM退出的時候銷毀。而其他的數據依賴于每一個線程,在線程創建時創建,在線程退出時銷毀。
程序計數器
程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
由于Java 虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。
此內存區域是唯一一個在Java 虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。
虛擬機棧
線程私有,它的生命周期與線程相同。虛擬機棧描述的是Java 方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態鏈接、方法出口等信息。
動畫是由一幀一幀圖片連續切換結果的結果而產生的,其實虛擬機的運行和動畫也類似,每個在虛擬機中運行的程序也是由許多的幀的切換產生的結果,只是這些幀里面存放的是方法的局部變量,操作數棧,動態鏈接,方法返回地址和一些額外的附加信息組成。每一個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。
對于執行引擎來說,活動線程中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法。執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。
局部變量表
局部變量表是一組變量值存儲空間,用于存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中確定了該方法所需要分配的最大局部變量表的容量。
局部變量表的容量以變量槽(Slot)為最小單位,32位虛擬機中一個Slot可以存放一個32位以內的數據類型(boolean、byte、char、short、int、float、reference和returnAddress八種)。
reference類型虛擬機規范沒有明確說明它的長度,但一般來說,虛擬機實現至少都應當能從此引用中直接或者間接地查找到對象在Java堆中的起始地址索引和方法區中的對象類型數據。
returnAddress類型是為字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,如果是實例方法(非static),那么局部變量表的第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用,在方法中通過this訪問。
Slot是可以重用的,當Slot中的變量超出了作用域,那么下一次分配Slot的時候,將會覆蓋原來的數據。Slot對對象的引用會影響GC(要是被引用,將不會被回收)。
** 系統不會為局部變量賦予初始值(實例變量和類變量都會被賦予初始值)**。也就是說不存在類變量那樣的準備階段。
操作數棧
和局部變量區一樣,操作數棧也是被組織成一個以字長為單位的數組。但是和前者不同的是,它不是通過索引來訪問,而是通過標準的棧操作——壓棧和出棧—來訪問的。比如,如果某個指令把一個值壓入到操作數棧中,稍后另一個指令就可以彈出這個值來使用。
虛擬機在操作數棧中存儲數據的方式和在局部變量區中是一樣的:如int、long、float、double、reference和returnType的存儲。對于byte、short以及char類型的值在壓入到操作數棧之前,也會被轉換為int。
虛擬機把操作數棧作為它的工作區——大多數指令都要從這里彈出數據,執行運算,然后把結果壓回操作數棧。比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中,看看下面的示例,它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
[plain] view plain copy
print?
begin
iload_0 // push the int in local variable 0 ontothe stack
iload_1 //push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
在這個字節碼序列里,前兩個指令iload_0和iload_1將存儲在局部變量中索引為0和1的整數壓入操作數棧中,其后iadd指令從操作數棧中彈出那兩個整數相加,再將結果壓入操作數棧。第四條指令istore_2則從操作數棧中彈出結果,并把它存儲到局部變量區索引為2的位置。下圖詳細表述了這個過程中局部變量和操作數棧的狀態變化,圖中沒有使用的局部變量區和操作數棧區域以空白表示。
動態連接
虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調用代表棧幀B的方法,那么這個虛擬機的方法調用指令就會以B方法的符號引用作為參數,但是因為符號引用并不是直接指向代表B方法的內存位置,所以在調用之前還必須要將符號引用轉換為直接引用,然后通過直接引用才可以訪問到真正的方法。
如果符號引用是在類加載階段或者第一次使用的時候轉化為直接應用,那么這種轉換成為靜態解析,如果是在運行期間轉換為直接引用,那么這種轉換就成為動態連接。
返回地址
方法的返回分為兩種情況,一種是正常退出,退出后會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法。
不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,如果方法是正常退出的,則調用者的PC計數器的值就可以作為返回地址,,果是因為異常退出的,則是需要通過異常處理表來確定。
方法的的一次調用就對應著棧幀在虛擬機棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括:恢復上層方法的局部變量表以及操作數棧,如果有返回值的話,就把返回值壓入到調用者棧幀的操作數棧中,還會把PC計數器的值調整為方法調用入口的下一條指令。
異常
在Java 虛擬機規范中,對虛擬機棧規定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError 異常;如果虛擬機棧可以動態擴展(當前大部分的Java 虛擬機都可動態擴展,只不過Java 虛擬機規范中也允許固定長度的虛擬機棧),當擴展時無法申請到足夠的內存時會拋出OutOfMemoryError 異常。
本地方法棧
本地方法棧(Native MethodStacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native 方法服務。虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二為一。
與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
堆
堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。
堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。
堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms為JVM啟動時申請的最小內存,默認為操作系統物理內存的1/64但小于1G,-Xmx為JVM可申請的最大內存,默認為物理內存的1/4但小于1G,默認當空余堆內存小于40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空余堆內存大于70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對于運行系統,為避免在運行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。
如果從內存回收的角度看,由于現在收集器基本都是采用的分代收集算法,所以Java 堆中還可以細分為:新生代和老年代;
新生代:程序新創建的對象都是從新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。
老年代:用于存放經過多次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啟動參數設置-XX:PretenureSizeThreshold=1024(單位為字節,默認為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數組對象,且數組中無引用外部對象。
老年代所占的內存大小為-Xmx對應的值減去-Xmn對應的值。
如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。
方法區
方法區在一個jvm實例的內部,類型信息被存儲在一個稱為方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。
簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用之前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、連接(驗證、準備、解析)和初始化。而裝載(后的結果就是由.class文件轉變為方法區中的一段特定的數據結構。這個數據結構會存儲如下信息:
類型信息
這個類型的全限定名
這個類型的直接超類的全限定名
這個類型是類類型還是接口類型
這個類型的訪問修飾符
任何直接超接口的全限定名的有序列表
字段信息
字段名
字段類型
字段的修飾符
方法信息
方法名
方法返回類型
方法參數的數量和類型(按照順序)
方法的修飾符
其他信息
除了常量以外的所有類(靜態)變量
一個指向ClassLoader的指針
一個指向Class對象的指針
常量池(常量數據以及對其他類型的符號引用)
JVM為每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。
每個類的這些元數據,無論是在構建這個類的實例還是調用這個類某個對象的方法,都會訪問方法區的這些元數據。
構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區獲得),注意,這里并不是僅僅為當前對象的實例屬性分配空間,還需要給父類的實例屬性分配,到此其實我們就可以回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另外一個角度也可以印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味著實例的創建,所以子類實例化時,父類肯定也會被實例化。
類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成為類數據在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區中為每個non-final類變量分配空間。
方法區主要有以下幾個特點:
1、方法區是線程安全的。由于所有的線程都共享方法區,所以,方法區里的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那么只允許一個線程去裝載它,而其它線程必須等待
2、方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。
3、方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集
可以通過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
對于習慣在HotSpot 虛擬機上開發和部署程序的開發者來說,很多人愿意把方法區稱為“永久代”(PermanentGeneration),本質上兩者并不等價,僅僅是因為HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對于其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
總結
名稱
特征
作用
配置參數
異常
程序計數器
占用內存小,線程私有,
生命周期與線程相同
大致為字節碼行號指示器
無
無
虛擬機棧
線程私有,生命周期與線程相同,使用連續的內存空間
Java 方法執行的內存模型,存儲局部變量表、操作棧、動態鏈接、方法出口等信息
-Xss
StackOverflowError
OutOfMemoryError
java堆
線程共享,生命周期與虛擬機相同,可以不使用連續的內存地址
保存對象實例,所有對象實例(包括數組)都要在堆上分配
-Xms
-Xsx
-Xmn
OutOfMemoryError
方法區
線程共享,生命周期與虛擬機相同,可以不使用連續的內存地址
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
-XX:PermSize:
16M
-XX:MaxPermSize
64M
OutOfMemoryError
運行時常量池
方法區的一部分,具有動態性
存放字面量及符號引用
直接內存
直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現,所以我們放到這里一起講解。
在JDK 1.4 中新加入了NIO(NewInput/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O 方式,它可以使用Native 函數庫直接分配堆外內存,然后通過一個存儲在Java 堆里面的DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java 堆和Native 堆中來回復制數據。
堆與棧的對比
經常有人把Java 內存區分為堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這復雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關系最密切的內存區域是這兩塊。
堆很靈活,但是不安全。對于對象,我們要動態地創建、銷毀,不能說后創建的對象沒有銷毀,先前創建的對象就不能銷毀,那樣的話我們的程序就寸步難行,所以Java中用堆來存儲對象。而一旦堆中的對象被銷毀,我們繼續引用這個對象的話,就會出現著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時才會被發現。
棧不靈活,但是很嚴格,是安全的,易于管理。因為只要上面的引用沒有銷毀,下面引用就一定還在,在大部分程序中,都是先定義的變量、引用先進棧,后定義的后進棧,同時,區塊內部的變量、引用在進入區塊時壓棧,區塊結束時出棧,理解了這種機制,我們就可以很方便地理解各種編程語言的作用域的概念了,同時這也是棧的優點——錯誤的引用邏輯在編譯時就可以被發現。
棧--主要存放引用和基本數據類型。
堆--用來存放 new 出來的對象實例。
內存溢出和內存泄漏
內存溢出 out of memory,是指程序在申請內存時,沒有足夠的內存空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是內存溢出。
內存泄露 memory leak,是指程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光。
memory leak會最終會導致out ofmemory。
Java 堆內存的OutOfMemoryError異常是實際應用中最常見的內存溢出異常情況。出現Java 堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟著進一步提示“Java heapspace”。
要解決這個區域的異常,一般的手段是首先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。
如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關聯并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots 引用鏈的信息,就可以比較準確地定位出泄漏代碼的位置。
如果不存在泄漏,換句話說就是內存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。
內存分配過程
1、JVM 會試圖為相關Java對象在Eden Space中初始化一塊內存區域。
2、當Eden空間足夠時,內存申請結束;否則到下一步。
3、JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。
4、Survivor區被用來作為Eden及Old的中間交換區域,當Old區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區。
5、當Old區空間不夠時,JVM 會在Old區進行完全的垃圾收集(0級)。
6、完全垃圾收集后,若Survivor及Old區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現“outofmemory”錯誤。
對象訪問
對象訪問在Java 語言中無處不在,是最普通的程序行為,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要內存區域之間的關聯關系,如下面的這句代碼:
Object obj = newObject();
假設這句代碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的本地變量表中,作為一個reference 類型數據出現。而“new Object()”這部分的語義將會反映到Java 堆中,形成一塊存儲了Object 類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存布局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。
由于reference 類型在Java 虛擬機規范里面只規定了一個指向對象的引用,并沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針。
如果使用句柄訪問方式,Java 堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。
學過C語言的朋友都知道C編譯器在劃分內存區域的時候經常將管理的區域劃分為數據段和代碼段,數據段包括堆、棧以及靜態數據區。那么在Java語言當中,內存又是如何劃分的呢?
由于Java程序是交由JVM執行的,所以我們在談Java內存區域劃分的時候事實上是指JVM內存區域劃分。在討論JVM內存區域劃分之前,先來看一下Java程序具體執行的過程:
如上圖所示,首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節碼文件(.class后綴),然后由JVM中的類加載器加載各個類的字節碼文件,加載完畢之后,交由JVM執行引擎執行。在整個程序執行過程中,JVM會用一段空間來存儲程序執行期間需要用到的數據和相關信息,這段空間一般被稱作為Runtime Data Area(運行時數據區),也就是我們常說的JVM內存。因此,在Java中我們常常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。
在知道了JVM內存是什么東西之后,下面我們就來討論一下這段空間具體是如何劃分區域的,是不是也像C語言中一樣也存在棧和堆呢?
一.運行時數據區包括哪幾部分?
根據《Java虛擬機規范》的規定,運行時數據區通常包括這幾個部分:程序計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。
如上圖所示,JVM中的運行時數據區應該包括這些部分。在JVM規范中雖然規定了程序在執行期間運行時數據區應該包括這幾部分,但是至于具體如何實現并沒有做出規定,不同的虛擬機廠商可以有不同的實現方式。
二.運行時數據區的每部分到底存儲了哪些數據?
下面我們來了解一下運行時數據區的每部分具體用來存儲程序執行過程中的哪些數據。
1.程序計數器
程序計數器(Program Counter Register),也有稱作為PC寄存器。想必學過匯編語言的朋友對程序計數器這個概念并不陌生,在匯編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址),當CPU需要執行指令時,需要從程序計數器中得到當前需要執行的指令所在存儲單元的地址,然后根據得到的地址獲取到指令,在得到指令之后,程序計數器便自動加1或者根據轉移指針得到下一條指令的地址,如此循環,直至執行完所有的指令。
雖然JVM中的程序計數器并不像匯編語言中的程序計數器一樣是物理概念上的CPU寄存器,但是JVM中的程序計數器的功能跟匯編語言中的程序計數器的功能在邏輯上是等同的,也就是說是用來指示 執行哪條指令的。
由于在JVM中,多線程是通過線程輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,因此,為了能夠使得每個線程都在線程切換后能夠恢復在切換之前的程序執行位置,每個線程都需要有自己獨立的程序計數器,并且不能互相被干擾,否則就會影響到程序的正常執行次序。因此,可以這么說,程序計數器是每個線程所私有的。
在JVM規范中規定,如果線程執行的是非native方法,則程序計數器中保存的是當前需要執行的指令的地址;如果線程執行的是native方法,則程序計數器中的值是undefined。
由于程序計數器中存儲的數據所占空間的大小不會隨程序的執行而發生改變,因此,對于程序計數器是不會發生內存溢出現象(OutOfMemory)的。
2.Java棧
Java棧也稱作虛擬機棧(Java Vitual Machine Stack),也就是我們常常所說的棧,跟C語言的數據段中的棧類似。事實上,Java棧是Java方法執行的內存模型。為什么這么說呢?下面就來解釋一下其中的原因。
Java棧中存放的是一個個的棧幀,每個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操作數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當線程執行一個方法時,就會隨之創建一個對應的棧幀,并將建立的棧幀壓棧。當方法執行完畢之后,便會將棧幀出棧。因此可知,線程當前執行的方法所對應的棧幀必定位于Java棧的頂部。講到這里,大家就應該會明白為什么 在 使用 遞歸方法的時候容易導致棧內存溢出的現象了以及為什么棧區的空間不用程序員去管理了(當然在Java中,程序員基本不用關系到內存分配和釋放的事情,因為Java有自己的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對于所有的程序設計語言來說,棧這部分空間對程序員來說是不透明的。下圖表示了一個Java棧的模型:
局部變量表,顧名思義,想必不用解釋大家應該明白它的作用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對于基本數據類型的變量,則直接存儲它的值,對于引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就可以確定其大小了,因此在程序執行期間局部變量表的大小是不會改變的。
操作數棧,想必學過數據結構中的棧的朋友想必對表達式求值問題不會陌生,棧最典型的一個應用就是用來對表達式求值。想想一個線程執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這么說,程序中的所有計算過程都是在借助于操作數棧來完成的。
指向運行時常量池的引用,因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向運行時常量。
方法返回地址,當一個方法執行完畢之后,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。
由于每個線程正在執行的方法可能不同,因此每個線程都會有一個自己的Java棧,互不干擾。
3.本地方法棧
本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的。在JVM規范中,并沒有對本地方發展的具體實現方法以及數據結構作強制規定,虛擬機可以自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一。
4.堆
在C語言中,堆這部分空間是唯一一個程序員可以管理的內存區域。程序員可以通過malloc函數和free函數在堆上申請和釋放空間。那么在Java中是怎么樣的呢?
Java中的堆是用來存儲對象本身的以及數組(當然,數組引用是存放在Java棧中的)。只不過和C語言中的不同,在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。因此這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被所有線程共享的,在JVM中只有一個堆。
5.方法區
方法區在JVM中也是一個非常重要的區域,它與堆一樣,是被線程共享的區域。在方法區中,存儲了每個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯后的代碼等。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區中有一個非常重要的部分就是運行時常量池,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后,對應的運行時常量池就被創建出來。當然并非Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,比如String的intern方法。
在JVM規范中,沒有強制要求方法區必須實現垃圾回收。很多人習慣將方法區稱為“永久代”,是因為HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾收集器可以像管理堆區一樣管理這部分區域,從而不需要專門為這部分設計垃圾回收機制。不過自從JDK7之后,Hotspot虛擬機便將運行時常量池從永久代移除了。
以上為個人看法和觀點,如有不正之處希望諒解并歡迎指正。
參考資料:
http://blog.csdn.net/ns_code/article/details/17565503
http://www.cnblogs.com/sunada2005/p/3577799.html
《深入理解Java虛擬機》
《Java虛擬機規范 SE7》