在首先,須要注意的是,Java內存區域與Java內存模型是不一樣的概念:html
ava虛擬機在運行程序時會把其自動管理的內存劃分爲區域,這些區域就被稱爲 Java內存區域。java
而Java內存模型(即Java Memory Model,簡稱JMM)自己是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。linux
而在Java內存區域與Java內存模型惟一一點類似之處,都存在共享數據區域和私有數據區域, 但二者的內存區域又不能等同,屬於不一樣層級的概念。算法
在查閱資料的時候,發現有人將這二者混爲一談,讓我頗感困惑,特意在這裏提醒一下。windows
而Java內存區域的概念比起JMM來講要淺顯易懂。數組
而Java內存區域則劃分爲: 方法區,堆(這二者是被全部線程共享的內存區域),虛擬機棧,本地方法棧,程序計數器。(這三者屬於線程隔離區域)緩存
但對於不一樣的虛擬機,實現可能有所區別。數據結構
在這裏的程序計數器,做用與計算機中的程序計數器十分相似,不過處理的對象有所不一樣,在計算機中,程序計數器指向的是下一條計算機指令所在的地址,而Java的程序計數器是一塊內存空間,能夠被看作是當前線程
所執行的字節碼文件的行號指示器。併發
當所執行的方法爲 Native時,這個程序計數器的值爲空。jvm
對於每個線程,都須要維護其獨有的程序計數器。
Java虛擬機棧也是線程所私有的,生命週期與線程相同。它描述的是Java方法執行的內存模型:在每一個方法執行的同時,都會建立一個棧幀,每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧出棧的過程。
通俗意義上講,就是當一個方法被調用的時候,表明這個方法的棧幀入棧,當一個方法返回的時候,表明這個方法的棧幀出棧。
棧幀(stack frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。
若是線程所請求的棧深度大於虛擬機的最大棧深度,會拋出 StackOverflowError異常,而若是虛擬的棧自己能夠動態擴展,在擴展時沒法申請到足夠內存,則拋出 OutOfMemoryError異常。
查看字節碼的命令:
javap -verbose ClassName.class
局部變量表
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性max_locals數據項中肯定了該方法所須要分配的最大局部變量表的容量。
局部變量表中的空間基本單位是 slot, 對於32位以內的數據,用一個 slot 來存放,如 int,short 等;對於64位的數據用連續的兩個 slot 來存放,如 long,double 等。引用類型的變量 JVM 並無規定其長度,它多是 32 位,也有多是 64 位的,因此既有可能佔一個 slot,也有可能佔兩個 slot。而且須要注意到的是,在這裏所提到的數據類型,僅僅是可以與java的數據類型類比,事實上依然分屬不一樣的概念。 JVM並不只僅只支持java語言,天然數據類型也不可能和java強相關。
虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程的,若是是實例方法(非static),那麼局部變量表的第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中經過this訪問。咱們知道,靜態方法只須要和類關聯便可,實例方法必須和對象相關聯。
而之因此在實例方法中能夠訪問其餘實例方法,是由於在實例方法中第一個參數爲this,指向當前對象,天然能夠調用其餘方法,而靜態方法沒有 this 引用,沒法給實例方法提供指向方法接收者的隱含參數,所以不能調用實例方法。
在局部變量表中存儲參數(索引從零開始)的順序爲:
this(若是是實例方法)=> 參數(若是有的話)=> 定義的局部變量(若是有的話)。
同時,局部變量表中的Slot是可重用的,方法體中定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼PC計數器的值已經超出了某個變量的做用域,那麼這個變量對應的Slot就能夠交給其餘變量使用。這樣的設計不只僅是爲了節省棧空間,在某些狀況下Slot的複用會直接影響到系統的垃圾收集行爲。
而這一點參考:
操做數棧
在這以前,首先須要提到一個概念。
Java虛擬機的解釋執行引擎被稱爲"基於棧的執行引擎"。
Java的一大特色是,一次編譯,處處運行,而這裏的編譯並不是編譯成機器碼,而僅僅是指編譯成字節碼文件,在不一樣的機器上都有相應的JVM執行這些字節碼文件,以求在不一樣的機器上會有相同的表現形式。
類裝載器裝載負責裝載編譯後的字節碼,並加載到運行時數據區(Runtime Data Area),而後執行引擎執行會執行這些經過類裝載器裝載的字節碼,被分配到JVM的運行時數據區的字節碼會被執行引擎執行。
這裏所說的基於棧執行是區別於基於寄存器執行的。
而這裏的執行引擎在我理解來就是讀取字節碼,解釋並執行相應代碼的功能。
但這種解釋不可避免的會致使效率低下,所以在Java中又使用了,即時編譯(JIT),將熱點代碼編譯成本地代碼,放入本地緩存中,在使用時直接讀取相應的本地代碼去執行便可,使得速度很是快。當再也不是熱點代碼,則從緩存中移除,恢復成解釋型執行。
在看過Java的執行引擎以後,再來看,對應的操做數棧。
和局部變量區同樣,操做數棧也是被組織成一個以slot爲單位的數組。可是和前者不一樣的是,它不是經過索引來訪問,而是經過標準的棧操做—壓棧和出棧—來訪問的。好比,若是某個指令把一個值壓入到操做數棧中,稍後另外一個指令就能夠彈出這個值來使用。
看下面這段代碼,簡單的將兩個數相加:
begin iload_0 // push the int in local variable 0 onto the 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
則是分別將操做數壓入棧,這裏從0開始,是由於靜態方法的參數,正是從索引0開始的, 將索引0,1的位置數據分別壓入棧,然後彈出兩個數,相加以後,再度壓入棧,存在索引2的位置。不難理解,在最終,操做數棧存有的即是方法自己的返回值。
動態鏈接
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。咱們知道Class文件的常量池有存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次的運行期間轉化爲直接引用,這部分稱爲動態鏈接。
本地方法棧是區別於虛擬機方法棧的,其區別不過是虛擬機方法棧是爲執行Java方法服務,而本地方法棧則是爲 Native方法服務,在HotSpot中,則是採起將二者合二爲一。
Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。這個內存區域惟一的做用就是用來存放Java實例對象,幾乎全部的實例對象都要在當前區域分配,數組是一種特殊類型的對象,也被分配在當前區域。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲「GC堆」(Garbage Collected Heap)。
從內存回收的角度來看,因爲如今收集器基本上都是採用分代收集算法回收內存,因此Java堆中還能夠細分爲:新生代和老年代;再細分一點的有:Eden空間,From Survivor空間,To Survivor空間等。
從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer ,TLAB),不管如何劃分,都與存放內容無關,不管哪一個區域,存放的都仍然是對象實例。進一步劃分是爲了更好地回收內存,更快地分配內存。
根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續便可,就像咱們的磁盤空間同樣。當前主流的虛擬機都是按照可擴展來實現的,若是在堆中沒有內存完成實例分配,而且堆也沒法在擴展時,將會拋出OutOfMemoryError異常。
至於更詳細的就不在這裏多作介紹,放在下一篇博客進行詳細介紹。
參考:
方法區被全部線程所共享,存儲已經被虛擬機加載的類信息,常量,靜態變量,即時編譯後的代碼等數據。
在HotSpot中用永久代的方式來實現方法區,而且將GC的分代收集擴展至方法區。至於其中缺點稍後再談。
須要注意到的是:
方法區的大小沒必要是固定的,jvm能夠根據應用的須要動態調整。一樣方法區也沒必要是連續的。方法區能夠在堆(甚至是虛擬機本身的堆)中分配。jvm能夠容許用戶和程序指定方法區的初始大小,最小和最大尺寸。
方法區一樣存在垃圾收集,由於經過用戶定義的類加載器能夠動態擴展Java程序,一些類也會成爲垃圾。jvm能夠回收一個未被引用類所佔的空間,以使方法區的空間最小。
接下來分別看一下,在方法區存儲的這幾種信息。
類信息
對於每一個已經被虛擬機加載的類,jvm在方法區中存儲如下類型信息:
這個類型的完整有效名稱(全限定名 包名+類名)
這個類型的直接父類的完整有效名稱(Interface 或者 java.lang.Object 沒有父類)
這個類型的修飾符(public abstract final )
這個類型直接接口的一個有序列表
類型的常量池( constant pool)
域(Field)信息
方法(Method)信息
除了常量外的全部靜態(static)變量
常量池
而常量則是存儲在常量池中,jvm爲每個被虛擬機加載的類型維護一個常量池,常量池就是這個類型裏用到的常量的一個有序集合:
包括了實際的常量(被final修飾的, 廣泛意義上的常量),對類、屬性、方法的符號引用。池中數據相似數組項,能夠經過索引訪問。
這個常量池,和運行時常量池是相同的概念,在運行時,當類被加載以後,相應內存才存放在運行時常量池中。
而就在這裏,有一個讓我以前混淆的概念問題,就是 import * 與import單個Java類的區別,我一直覺得,import * 會引入全部Java類的常量數據,相關的final信息,致使佔用沒必要要的內存,由於這樣的緣由才致使須要引入特定類。
但事實顯然並不是如此,import自己只能影響編譯,對運行時無能爲力,所以只會帶來編譯時的壓力,而不會引發其餘問題。
但仍是推崇引入單個Java類這種方式:
域信息
jvm在方法區中保存類的全部域的相關信息以及聲明順序。
域的相關信息:
域名
域的類型
域的修飾符
在Java中域是指:
field,域是一種屬性,能夠是一個類變量,一個對象變量,一個對象方法變量或者是一個函數的參數。
因此這個類自身的幾乎全部信息都可以在方法區中找到。
方法信息:
jvm在方法區中保存類的全部方法的相關信息以及聲明順序方法的相關信息:
方法的返回值(或者void)
方法的參數列表
方法的修飾符
方法的字節碼操做數棧和局部變量表大小
異常表
在這裏主要了解一下,在Java堆中,一個對象是如何被分配,佈局,訪問,這整個過程的。
當使用一個方法,且在方法中定義一個變量時,這個變量的引用/變量,便會被存儲在Java虛擬機棧中的 棧幀 中的 局部變量表中,Java棧是線程私有的,當變量指向一個 new 出來的對象時,會在共享區域,也就是方法區中的常量池中去查找相應類的符號引用,並檢測類是否被加載。
而在類被加載以後,須要給新生對象分配相應的內存,若是在堆中,內存劃分工整有序,被分爲空閒的和已使用的內存,中間經過一個指針來進行分割,那麼分配內存就是移動指針便可,而若是內存混亂無序,就須要維護相應的列表,記錄內存的使用狀況,前者被稱爲「指針碰撞」,後者被曾爲「空閒列表」,而Java虛擬機究竟採起哪一種方式的關鍵因素之一則是: 垃圾收集器是否具備內存壓縮功能。
而當內存已經指定,在劃份內存區域的時候一樣會遇到相應的併發情況,這時候就須要採起CAS的方式, 或是採起將分配動做按照線程劃分不一樣的區域,保證不會衝突。
而在內存分配完畢以後,虛擬機須要將分配到的空間初始化爲零值(不包含對象頭),這一操做保證了對象的實例字段在Java代碼中即便不進行初始化也可以直接使用。
接下來就是須要設置對象頭,如對象是哪一個類的實例(與多態息息相關),如何可以找到對象的元信息等其餘信息。
而這時,以Java虛擬的角度來看,對象的建立已經完成。但從Java程序的角度來看,對象的建立纔剛剛開始,進行相應的初始化操做等。
而一個Java對象分爲三塊區域:對象頭,實例數據,對齊填充。
對象頭主要分爲兩部分:其一是對象的運行時數據,如哈希碼,GC分代年齡,鎖狀態,等其餘信息。
而另外一部分則是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個來肯定到底是哪一個類的實例,若是爲數組對象,則還須要記錄數組長度的數據。
而實例數據則是對象真正存儲的有效信息。
對齊填充的真正目的在於由於在hotSpot內存管理中要求對象起始地址必須爲8字節的整數倍。
在對象的訪問中,主流存在兩種方式:
句柄:在Java棧中存儲的是對象的句柄,在對象的句柄中包含對象的實例數據,對象類型信息的地址。 對象的實例數據地址再指向堆中的實例數據。對象類型指針則是指向方法區中的對象類型信息。
直接指針:在棧中存儲的就是對象地址,對象地址中包含對象的實例數據以及到對象類型數據的指針。
以上就是堆中對象的相關信息。
來源於:《深刻理解Java虛擬機》
-Xms -Xmx
用來分配和設置進程對堆內存的最小最大內存大小。
-Xmn
-Xmn用來設置堆內新生代的大小。經過這個值咱們也能夠獲得老生代的大小:-Xmx減去-Xmn,在JDK1.8之前,HotSpot中還要減去永久代的大小
-Xss
-Xss設置每一個線程可以使用的內存大小。在相同物理內存下,減少這個值能生成更多的線程。所以對於具備多個線程的應用而言,須要將這個值設置的儘可能小,以支持更多的線程。
而關於內存的限制,不得不提到的一個點是:
32位程序的尋址能力是2^32,也就是4G。對於32位程序只能申請到4G的內存。並且這4G內存中,在windows下有2G,linux下有1G是保留給內核態使用,用戶態沒法訪問。故只能分配2G、3G的內存使用(對單個進程而言)。
參考:
Java8內存模型—永久代(PermGen)和元空間(Metaspace)
之因此要將永久代單獨提出來, 是由於在 java8中完全移除了一個概念,永久代。而其緣由除了永久代自己的設計不合理因素以外, 則是Oracle可能要將 HotSpot 與 JRocket進行合併,所以這種特性上的,且設計並不是很合理的東西就被刪除掉了。
那麼永久代的概念是什麼呢?
其實在以前已經提到過這個概念了,也就是方法區,不過有所不一樣的是,方法區是Jvm的一種規範,而永久代則是方法區在HotSpot中的實現方式。而最初採起這種設計方式的目的,則是由於,在方法區一樣須要實現類的卸載,運行時常量池的回收工做。(須要瞭解的是,字符串常量就被存儲在永久代中。)
所以就將GC的分代收集擴展至方法區,使用永久代來實現方法區。
java.lang.OutOfMemoryError: PermGen space "這個異常正是因爲永久代所致使的。最典型的場景就是,在 jsp 頁面比較多的狀況,容易出現永久代內存溢出。由於jsp頁面在其所對應的servlet第一次被訪問時,就會建立相應的class文件。而且加載到方法區中。
而原先的運行時常量則是存儲到元空間。
字符串池裏的內容是在類加載完成,通過驗證,準備階段以後在堆中生成字符串對象實例,而後將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開闢的一塊空間存放的)。 在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個哈希表,裏面存的是駐留字符串(也就是咱們常說的用雙引號括起來的)的引用(而不是駐留字符串實例自己),也就是說在堆中的某些字符串實例被這個StringTable引用以後就等同被賦予了」駐留字符串」的身份。這個StringTable在每一個HotSpot VM的實例只有一份,被全部的類共享。
對於字符串常量池:
而元空間:
元空間與永久代相似,存放的數據差異不大,最大的區別則在於存放的位置,元空間並不在虛擬機中,而是使用本地內存。
相關配置參數:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的(僅取決於自身機器內存影響)。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集。
所以在無限加載String時, Java Heap Space
而加載大量Java類時: MetaSpace。
JVM內存區域相關的介紹,就到這裏。