JVM的內存區域劃分

說到Java內存區域,可能不少人第一反應是「堆棧」。首先堆棧不是一個概念,而是兩個概念,堆和棧是兩塊不一樣的內存區域,簡單理解的話,堆是用來存放對象而棧是用來執行程序的。其次,堆內存和棧內存的這種劃分方式比較粗糙,這種劃分方式只能說明大多數程序員最關注的、與對象內存分配關係最密切的內存區域是這兩塊,Java內存區域的劃分實際上遠比這複雜。對於Java程序員來講,在虛擬機自動內存管理機制的幫助下,再也不須要爲每個new操做去配對delete/free代碼,不容易出現內存泄露和內存溢出問題。可是,也正是由於Java把內存控制權交給了虛擬機,一旦出現內存泄露和內存溢出的問題,就難以排查,所以一個好的Java程序員應該去了解虛擬機的內存區域以及會引發內存泄露和內存溢出的場景。java

1、運行時內存區域程序員

Java虛擬機(JVM)內部定義了程序在運行時須要使用到的內存區域算法

JVM的內存區域劃分

之因此要劃分這麼多區域出來是由於這些區域都有本身的用途,以及建立和銷燬的時間。有些區域隨着虛擬機進程的啓動而存在,有的區域則依賴用戶線程的啓動和結束而銷燬和創建。圖中綠色部分就是全部線程之間共享的內存區域,而其他部分則是線程運行時獨有的數據區域,從這個分類角度來看一下這幾個數據區。安全

一、線程獨有的內存區域服務器

(1)PROGRAM COUNTER REGISTER,程序計數器markdown

這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器經過改變這個計數器的值來選取下一條須要執行的字節碼指令。數據結構

在JVM規範中規定,若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令的地址;若是線程執行的是native方法,則程序計數器中的值是undefined。ide

因爲程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,所以,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。函數

總結:
當前線程所執行的字節碼的行號指示器;
當前線程私有;
不會出現OutOfMemoryError狀況。佈局

(2)JAVA STACK,虛擬機棧

Java棧也稱做虛擬機棧(Java Vitual Machine Stack),也就是咱們經常所說的棧,跟C語言的數據段中的棧相似。事實上,Java棧是Java方法執行的內存模型。爲何這麼說呢?下面就來解釋一下其中的緣由。

  Java棧中存放的是一個個的棧幀,每一個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操做數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當線程執行一個方法時,就會隨之建立一個對應的棧幀,並將創建的棧幀壓棧。當方法執行完畢以後,便會將棧幀出棧。所以可知,線程當前執行的方法所對應的棧幀一定位於Java棧的頂部。講到這裏,你們就應該會明白爲何在使用遞歸方法的時候容易致使棧內存溢出的現象了以及爲何棧區的空間不用程序員去管理了(固然在Java中,程序員基本不用關係到內存分配和釋放的事情,由於Java有本身的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對於全部的程序設計語言來講,棧這部分空間對程序員來講是不透明的。下圖表示了一個Java棧的模型:

JVM的內存區域劃分

局部變量表,顧名思義,想必不用解釋你們應該明白它的做用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就能夠肯定其大小了,所以在程序執行期間局部變量表的大小是不會改變的。

  操做數棧,想必學過數據結構中的棧的朋友想必對錶達式求值問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。想一想一個線程執行方法的過程當中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。所以能夠這麼說,程序中的全部計算過程都是在藉助於操做數棧來完成的。

  指向運行時常量池的引用,由於在方法執行的過程當中有可能須要用到類中的常量,因此必需要有一個引用指向運行時常量。

  方法返回地址,當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。

  因爲每一個線程正在執行的方法可能不一樣,所以每一個線程都會有一個本身的Java棧,互不干擾。

生命週期和線程相同。每一個方法執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息,每個方法從調用直至執行完畢的過程,就對應着一個棧幀在虛擬機中入棧到出棧的過程。棧的大小和具體JVM的實現有關,一般在256K~756K之間。

總結:
線程私有,生命週期與線程相同;
java方法執行的內存模型,每一個方法執行的同時都會建立一個棧幀,存儲局部變量表(基本類型、對象引用)、操做數棧、動態連接、方法出口等信息;
StackOverflowError異常:當線程請求的棧深度大於虛擬機所容許的深度;
OutOfMemoryError異常:若是棧的擴展時沒法申請到足夠的內存。

(3)NATIVE METHOD STACK,本地方法棧

本地方法棧與Java棧的做用和原理很是類似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並無對本地方發展的具體實現方法以及數據結構做強制規定,虛擬機能夠自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一。

二、線程間共享的內存區域

(1)HEAP,堆

大多數應用,堆都是Java虛擬機所管理的內存中最大的一塊,它在虛擬機啓動時建立,此內存惟一的目的就是存放對象實例。因爲如今垃圾收集器採用的基本都是分代收集算法,因此堆還能夠細分爲新生代和老年代,再細緻一點還有Eden區、From Survivior區、To Survivor區。

總結:
能夠經過-Xmx和-Xms控制堆的大小;
OutOfMemoryError異常:當在堆中沒有內存完成實例分配,且堆也沒法再擴展時。

(2)METHOD AREA,方法區

這塊區域用於存儲虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,虛擬機規範是把這塊區域描述爲堆的一個邏輯部分的,但實際它應該是要和堆區分開的。從上面提到的分代收集算法的角度看,HotSpot中,方法區≈永久代。不過JDK 7以後,咱們使用的HotSpot應該就沒有永久代這個概念了,會採用Native Memory來實現方法區的規劃了。

總結:
線程間共享;
用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據;
OutOfMemoryError異常:當方法區沒法知足內存的分配需求時。

(3)RUNTIME CONSTANT POOL,運行時常量池

上面的圖中沒有畫出來,由於它是方法區的一部分。Class文件中除了有類的版本信息、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯期間生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中,另外翻譯出來的直接引用也會存儲在這個區域中。這個區域另一個特色就是動態性,Java並不要求常量就必定要在編譯期間才能產生,運行期間也能夠在這個區域放入新的內容,String.intern()方法就是這個特性的應用。

總結:
方法區的一部分;
用於存放編譯期生成的各類字面量與符號引用;
OutOfMemoryError異常:當常量池沒法再申請到內存時。

三、直接內存

直接內存並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也被頻繁地使用,並且也可能致使內存溢出問題。JDK1.4中新增長了NIO,引入了一種基於通道與緩衝區的I/O方式,它可使用Native函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在Java堆和Native堆中來回複製數據。顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存(包括RAM、SWAP區)大小以及處理器尋址空間的限制。

總結:
NIO可使用Native函數庫直接分配堆外內存,堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做;
大小不受Java堆大小的限制,受本機(服務器)內存限制;
OutOfMemoryError異常:系統內存不足時。

2、對象的建立

Java是一門面向對象的語言,Java程序運行過程當中無時無刻都有對象被建立出來。在語言層面上,建立對象(克隆、反序列化)就是一個new關鍵字而已,可是虛擬機層面上卻不是如此。看一下在虛擬機層面上建立對象的步驟:

一、虛擬機遇到一條new指令,首先去檢查這個指令的參數可否在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化。若是沒有,那麼必須先執行類的初始化過程。

二、類加載檢查經過後,虛擬機爲新生對象分配內存。對象所需內存大小在類加載完成後即可以徹底肯定,爲對象分配空間無非就是從Java堆中劃分出一塊肯定大小的內存而已。這個地方會有兩個問題:

(1)若是內存是規整的,那麼虛擬機將採用的是指針碰撞法來爲對象分配內存。意思是全部用過的內存在一邊,空閒的內存在另一邊,中間放着一個指針做爲分界點的指示器,分配內存就僅僅是把指針向空閒那邊挪動一段與對象大小相等的距離罷了。若是垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。

(2)若是內存不是規整的,已使用的內存和未使用的內存相互交錯,那麼虛擬機將採用的是空閒列表法來爲對象分配內存。意思是虛擬機維護了一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。若是垃圾收集器選擇的是CMS這種基於標記-清除算法的,虛擬機採用這種分配方式。

另一個問題及時保證new對象時候的線程安全性。由於可能出現虛擬機正在給對象A分配內存,指針尚未來得及修改,對象B又同時使用了原來的指針來分配內存的狀況。虛擬機採用了CAS配上失敗重試的方式保證更新更新操做的原子性和TLAB兩種方式來解決這個問題。

三、內存分配結束,虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭)。這一步保證了對象的實例字段在Java代碼中能夠不用賦初始值就能夠直接使用,程序能訪問到這些字段的數據類型所對應的零值。

四、對對象進行必要的設置,例如這個對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,這些信息存放在對象的對象頭中。

五、執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

3、對象的訪問定位

創建對象是爲了使用對象,Java程序須要經過棧上的reference(引用)數據來操做堆上的具體對象。好比咱們寫了一句

Object obj = new Object()

而new Object()以後其實有兩部份內容,一部分是類數據(好比表明類的Class對象)、一部分是實例數據。

因爲reference在Java虛擬機規範中只是一個指向對象new Object()的引用obj,並無規定obj應該經過何種方式去定位、訪問堆中對象的具體位置,因此對象訪問方式也是取決於虛擬機而定的。主流方式有兩種:

一、句柄訪問。java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

二、指針訪問。java堆對象的佈局中必須考慮如何放置訪問類型數據的相關信息,reference中存儲的就是對象地址。

HotSpot虛擬機採用的是後者,不過前者的對象訪問方式也是十分常見的。

相關文章
相關標籤/搜索