JVM內存模型如上圖,須要聲明一點,這是《Java虛擬機規範(Java SE 7版)》規定的內容,實際區域由各JVM本身實現,因此可能略有不一樣。如下對各區域進行簡短說明。java
程序計數器是衆多編程語言都共有的一部分,做用是標示下一條須要執行的指令的位置,分支、循環、跳轉、異常處理、線程恢復等基礎功能都是依賴程序計數器完成的。算法
對於Java的多線程程序而言,不一樣的線程都是經過輪流得到cpu的時間片運行的,這符合計算機組成原理的基本概念,所以不一樣的線程之間須要不停的得到運行,掛起等待運行,因此各線程之間的計數器互不影響,獨立存儲。這些數據區屬於線程私有的內存。編程
VM虛擬機棧也是線程私有的,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法調用直至執行完的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。多線程
有人將java內存區域劃分爲棧與堆兩部分,在這種粗略的劃分下,棧標示的就是當前講的虛擬機棧,或者是虛擬機棧對應的局部變量表。之因此說這種劃分比較粗略是角度不一樣,這種劃分方法關心的是新申請內存的存在空間,而咱們目前談論的是JVM總體的內存劃分,因爲角度不一樣,因此劃分的方法不一樣,沒有對與錯。編程語言
局部變量表存放了編譯期可知的各類基本類型,對象引用,和returnAddress。其中64位長的long和double佔用了2個局部變量空間(slot),其餘類型都佔用1個。這也從存儲的角度上說明了long與double本質上的非原子性。局部變量表所需的內存在編譯期間完成分配,當進入一個方法時,這個方法在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表大小。函數
因爲棧幀的進出棧,顯而易見的帶來了空間分配上的問題。若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverFlowError異常;若是虛擬機棧能夠擴展,擴展時沒法申請到足夠的內存,將會拋出OutOfMemoryError。顯然,這種狀況大多數是因爲循環調用與遞歸帶來的。性能
本地方法棧與虛擬機棧的做用十分相似,不過本地方法是爲native方法服務的。部分虛擬機(好比 Sun HotSpot虛擬機)直接將本地方法棧與虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧也會拋出StactOverFlowError與OutOfMemoryError異常。優化
至此,線程私有數據區域結束,下面開始線程共享數據區。ui
Java堆是虛擬機所管理的內存中最大的一塊,在虛擬機啓動時建立,此塊內存的惟一目的就是存放對象實例,幾乎全部的對象實例都在對上分配內存。JVM規範中的描述是:全部的對象實例以及數據都要在堆上分配。可是隨着JIT編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配(對象只存在於某方法中,不會逃逸出去,所以方法出棧後就會銷燬,此時對象能夠在棧上分配,方便銷燬),標量替換(新對象擁有的屬性能夠由現有對象替換拼湊而成,就不必真正生成這個對象)等優化技術帶來了一些變化,目前並不是全部的對象都在堆上分配了。spa
當java堆上沒有內存完成實例分配,而且堆大小也沒法擴展是,將會拋出OutOfMemoryError異常。Java堆是垃圾收集器管理的主要區域。
方法區與java堆同樣,是線程共享的數據區,用於存儲被虛擬機加載的類信息、常量、靜態變量、即時編譯的代碼。JVM規範將方法與堆區分開,可是HotSpot將方法區做爲永久代(Permanent Generation)實現。這樣方便將GC分代手機方法擴展至方法區,HotSpot的垃圾收集器能夠像管理Java堆同樣管理方法區。可是這種方向已經逐步在被HotSpot替換中,在JDK1.7的版本中,已經把本來存放在方法區的字符串常量區移出。
至此,JVM規範所聲明的內存模型已經分析完畢,下面將分析一些常常提到的與內存相關的區域。
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等信息外,還有一項信息是常量池(Constant Poll Table)用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池存放。
其中字符串常量池屬於運行時常量池的一部分,不過在HotSpot虛擬機中,JDK1.7將字符串常量池移到了java堆中,經過下面的實驗能夠很容易看到。
import java.util.ArrayList; import java.util.List; /** * Created by shining.cui on 2017/7/23. */ public class RunTimeContantPoolOOM { public static void main(String[] args) { List list = new ArrayList(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } } }
在jdk1.6中,字符串常量區是在Perm Space中的,因此能夠將Perm Spacce設置的小一些,XX:MaxPermSize=10M能夠很快拋出異常:java.lang.OutOfMemoryError:Perm Space。
在jdk1.7以上,字符串常量區已經移到了Java堆中,設置-Xms:64m -Xmx:64m,很快就能夠拋出異常java.lang.OutOfMemoryError:java.heap.space。
直接內存不是JVM運行時的數據區的一部分,也不是Java虛擬機規範中定義的內存區域。在JDK1.4中引入了NIO(New Input/Output)類,引入了一種基於通道(Chanel)與緩衝區(Buffer)的I/O方式,他可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java中的DirectByteBuffer對象做爲對這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java對和Native對中來回複製數據。
最基礎的垃圾收集算法是「標記-清除」(Mark Sweep)算法,正如名字同樣,算法分爲2個階段:1.標記處須要回收的對象,2.回收被標記的對象。標記算法分爲兩種:1.引用計數算法(Reference Counting) 2.可達性分析算法(Reachability Analysis)。因爲引用技術算法沒法解決循環引用的問題,因此這裏使用的標記算法均爲可達性分析算法。
如圖所示,當進行過標記清除算法以後,出現了大量的非連續內存。當java堆須要分配一段連續的內存給一個新對象時,發現雖然內存清理出了不少的空閒,可是仍然須要繼續清理以知足「連續空間」的要求。因此說,這種方法比較基礎,效率也比較低下。
爲了解決效率與內存碎片問題,複製(Copying)算法出現了,它將內存劃分爲兩塊相等的大小,每次使用一塊,當這一塊用完了,就講還存活的對象複製到另一塊內存區域中,而後將當前內存空間一次性清理掉。這樣的對整個半區進行回收,分配時按照順序從內存頂端依次分配,這種實現簡單,運行高效。不過這種算法將原有的內存空間減小爲實際的一半,代價比較高。
從圖中能夠看出,整理後的內存十分規整,可是白白浪費通常的內存成本過高。然而這實際上是很重要的一個收集算法,由於如今的商業虛擬機都採用這種算法來回收新生代。IBM公司的專門研究代表,新生代中的對象98%都是「朝生夕死」的,因此不須要按照1:1的比例來劃份內存。HotSpot虛擬機將Java堆劃分爲年輕代(Young Generation)、老年代(Tenured Generation),其中年輕代又分爲一塊Eden和兩塊Survivor。
全部的新建對象都放在年輕代中,年輕代使用的GC算法就是複製算法。其中Eden與Survivor的內存大小比例爲8:2,其中Eden由1大塊組成,Survivor由2小塊組成。每次使用內存爲1Eden+1Survivor,即90%的內存。因爲年輕代中的對象生命週期每每很短,因此當須要進行GC的時候就將當前90%中存活的對象複製到另一塊Survivor中,原來的Eden與Survivor將被清空。可是這就有一個問題,咱們沒法保證每次年輕代GC後存活的對象都不高於10%。因此在當活下來的對象高於10%的時候,這部分對象將由Tenured進行擔保,即沒法複製到Survivor中的對象將移動到老年代。
複製算法在極端狀況下(存活對象較多)效率變得很低,而且須要有額外的空間進行分配擔保。因此在老年代中這種狀況通常是不適合的。
因此就出現了標記-整理(Mark-Compact)算法。與標記清除算法同樣,首先是標記對象,然而第二步是將存貨的對象向內存一段移動,整理出一塊較大的連續內存空間。