開始先說明一個知識點,Java虛擬機的內存結構和內存模型(JMM)實際上是兩個不同的東西,前者是下面要講的堆棧之類的內容,後者能夠看這篇文章。java
因爲全局字符串常量池(string pool或string literal pool) 在Java不一樣的版本中存放在不一樣的位置,下面以Java8爲例。程序員
Java虛擬機在執行Java程序的過程當中會把它管理的內存分爲若干個數據區域,每一個區域都有本身各自的用途和生命週期,有些區域歲虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。 根據 《Java虛擬機規範》的規定,Java虛擬機所管理的內存會包含如下幾個運行時數據區域,以下圖所示。可是要記住這裏只是一種規範,例如方法區是JVM的規範,但Java 1.8之前的**永久代(PermGen space)**和Java 1.8的 **元空間(Metaspace)**則是對這種規範的 實現。 算法
對大多數應用來講,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是全部線程共享的一塊內存區域,在虛擬機啓動的時候建立。改內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存,Java虛擬機規範中的描述是:全部對象以及數組都要在對上分配
。可是隨着JIT編譯器的發展和逃逸技術分析技術逐漸成熟,站上非配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都分配在對上也漸漸的變得不是那麼絕對了。數組
Java堆也是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」。目前內存回收基本都採用分帶手機算法,因此Java堆還能夠細分爲新生代和老生代
,再細一點還能夠分爲Eden空間
、From Survivor空間
、To Survivor空間
等。緩存
由於Java堆是線程共享的,因此Java堆可能劃分多個線程私有的分配緩存區(Thread Local Allocation Buffer =TLAB),不過不管怎麼劃分,其存放的仍是對象實例,進一步劃分的目的是爲了更好的回收內存,或者更快的分配內存。(TODO:垃圾回收和內存分配)bash
根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。在實際操做室,咱們能夠經過-Xmx和-Xms實現大小擴展。 若是堆中沒有足夠的內存來分配建立的實例,而且堆也沒法再擴展時,將會拋出OutOfMemoryError
異常。服務器
方法區同Java堆同樣,是各個線程共享的內存區域,主要用於存儲已被虛擬機加載的類信息,常量、靜態變量、即時編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲對的一個邏輯部分,可是它有一個別名叫Non-Heap(非堆),目的應該是與堆區分開來。數據結構
對於HosSpost來講,更多人把方法區稱爲「永久代(PermGen space)」,本質上二者並不相等,僅僅由於HotSpot虛擬機的設計團隊選擇把GC分帶收集擴展到方法區,或者說永久代是方法區的一個實現方式。這樣HotSpot的垃圾收集器能夠像管理Java堆同樣管理這部份內存,可以節省去專門寫管理方法區內存的代碼的工做,可是如何實現方法區屬於虛擬機的實現細節,不受Java虛擬機規範約束,相似BEA、JRockit、IBM J9虛擬機就不存在永久代的概念。多線程
Java虛擬機規範對方法區的限制很是寬鬆,和Java堆同樣不須要連續的內存,能夠再JVM啓動時設定固定的大小和可擴展,還能夠選擇不實現垃圾收集。可是並不意味着數據進入了方法區就如同永久代這個名字同樣「永久」存在了,這個區域的回收目標主要針對常量池的回收和對類型的卸載(TODO),通常來講,這部分區域的回收的能力至關弱,尤爲是類型的卸載,條件至關苛刻,可是這部分區域回收也確實是必要的。函數
永生代的移除從Java7就慢慢開始了,符號引用(Symbols)轉移到了native heap,字面量(interned strings)轉移到了java heap,類的靜態變量(class statics)轉移到了java heap。 在Java8中,用元空間替代了永生代。不一樣於在堆上分配空間的永生代,元空間的數據存儲在本地內存(Native Memory)中,並不存在於虛擬機中,這樣也就不會出現OutOfMemoryError
異常。
這裏說一個最重要的點,就是字符串常量池,在JDK6的時候存在於永生代中,其內存放對象。在JDK7以及之後存在於堆中,其內存放字面量(interned strings)和字符串在堆上的內存地址(也稱字符串引用),但String對象是所有處於堆上。用存於本地內存的StringTale來保存字符串引用,在調用String.intern()時會檢測該字符串,若是字符串常量池中未存在該字符串的引用,添加並保存改字符串在堆上的地址,若是字符串常量池中存在引用則直接返回,避免產生新的String的開銷。其結構相似於咱們經常使用的hashtable。
-XX:PermSize
方法區初始大小
-XX:MaxPermSize
方法區最大大小
超過這個值將會拋出OutOfMemoryError異常:java.lang.OutOfMemoryError: PermGen
-XX:MetaspaceSize 初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:
若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize最大空間,默認是沒有限制的。
-XX:MinMetaspaceFreeRatio在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集
複製代碼
程序計數器是一塊較小的內存空間,它能夠看作當前線程所執行字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間(時間片)來實現的,在仍和一個時刻,一個處理都會執行一個線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置每條線程都須要有一個獨立的程序計數器,各個線程之間的計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器值則爲空(Undenfined)。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OOM異常的區域。
同程序計數器同樣,Java虛擬機棧也是線程私有的,聽他的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同事都會建立一個棧幀(Stack Frame) 用於存儲局部變量表、操做數棧、動態連接、方法出口(ReturnAddress)等信息。每個方法從調用直至執行完成的過程,都對應着一個棧幀在虛擬機中入棧到出棧的過程。
常常有人把Java內存區分爲堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這複雜。這種劃分方式的流行只能說明大多數程序員最關注、與對象內存分配最密切的內存區域是這兩塊。其中所指的「堆」在上面的Java堆有簡要描述,但其內容遠遠不止如此,而「棧」則就是如今所講的虛擬機棧,或者說是虛擬機棧中局部變量表部分。
局部變量表存放了編譯器可知的各類基本數據類型(boolean、byte、插入、short、int、float、long、double
)、對象引用(reference
類型,他不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)和returnAddress
類型(指向了一條字節碼指令的地址)。
在Class文件中的常量持中存有大量的符號引用。字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用一部分在類的加載階段(解析)或第一次使用的時候就轉化爲了直接引用(指向數據所存地址的指針或句柄等),這種轉化稱爲靜態連接。而相反的,另外一部分在運行期間轉化爲直接引用,就稱爲動態連接。
其中64位長度的long
和double
類型的數據會佔用兩個局部變量控件(Slot
),其他的數據類型只佔用1個。局部變量所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
在Java虛擬機規範中,對於這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機多容許的深度,將拋出StackOverflowError
異常(最典型的就是遞歸時沒有設置好終止值);若是虛擬機棧能夠動態擴展(當前大部分的Java虛擬機均可動態擴展,只不過Java虛擬機規範中也容許固定長度的虛擬機棧),若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError
異常。
本地方法棧與虛擬機棧所發揮的做用是很是類似的,他們的區別不過是虛擬機棧執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到Native方法服務。虛擬機規範中沒有對本地方法棧中使用的語言、方式和數據結構做強制規定,具體的內容由虛擬機自由實現。甚至有的虛擬機(Sun HotSpot)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError
和OutOfMemoryError
異常。
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。可是這部份內存也被頻繁使用,並且也可能致使OutOfMemoryError
異常出現,因此這裏簡單描述下。
在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)和緩存區(Buffer)的I/O方式,他可使用Native
函數庫直接分配堆外內存,而後經過一個存儲在Java堆中的DirectByteBuffer
對象做爲這塊內存的引用進行操做。這樣能在一些場景生顯著提升性能,由於避免了在Java
堆Native
堆中來回賦值數據。
顯然,本級直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略內存,是的各個內存區域綜合大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現的OutOfMemoryError
異常。
本片內容絕大部分來自於周志明版的深刻理解Java虛擬機,看了一些系列的文章,發現仍是書裏講的比較細且容易懂,這裏只是搬運下,書中運行時常量池這一塊並無寫下來,是由於後續想針對這個作一個更詳細的總結。