在一開始學習java的時候,那時候是在網上看視頻,老師就常常提到什麼對象分配在堆區,什麼在棧區,那時候和理解,後來理解了就想着寫一篇文章好好的去梳理一下。java
這個內存結構是基於java8的內存結構,最文章末尾也會和java7的內存結構進行一個比較,看看哪些地方進行了改變,這些改變對性能的影響是什麼。數據結構
還有一點這個是基於Hotspot虛擬機來講的。app
先給一張java8的內存結構圖吧(我用Windows裏面的畫圖工具畫的)jvm
首先對這個圖有一個認識,從上面能夠看到java8的內存結構大體分了五個部分:PC寄存器,java虛擬機棧、本地方法棧、java堆、方法區。其中PC寄存器、java虛擬機棧和本地方法棧是全部線程共享的一塊內存區域。java堆和方法區是每個線程獨享的一塊區域,還有一個運行時常量池。
ide
接下來看一看每一塊區域裏面存放的什麼?工具
1、PC寄存器性能
在大學的時候學過計算機組成原理的時候都知道,內存裏面有不少寄存器,大概幾百個吧(目前),每一種寄存器的用途都不同,其中有一個寄存器就是程序計數器。這個寄存器的主要做用就是存放下一條須要執行的指令。這是由於在一個時刻,一個處理器只能執行一條線程中的指令。就比如說咱們的程序代碼假如是一行一行執行的,程序計數器永遠指向下一行須要執行的字節碼指令。在循環結構中,咱們就能夠改變程序計數器中的值,來改變下一條須要執行的指令。而且每個線程都有一個程序計數器。若是當前執行的是 Java 的方法,則該寄存器中保存當前執行指令的地址;假若執行的是native 方法,則PC寄存器中爲空(Undefined)。PC寄存區區域就是存放了N多個這樣的寄存區。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。所以能夠把他的幾個特色概括以下。學習
程序計數器指定下一條須要執行的指令測試
每個線程獨享一個程序寄存器優化
執行java代碼時,寄存器保存當前指令地址
執行native方法時候,寄存器爲空。
不會形成OutOfMemoryError狀況
2、Java虛擬機棧
虛擬機棧描述的是Java方法執行的內存模型,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。它的生命週期與線程相同。每一個線程有一個私有的棧,隨着線程的建立而建立。棧裏面存着的是一種叫「棧幀」的東西,每一個方法會建立一個棧幀,棧幀中存放了局部變量表(基本數據類型和對象引用)、操做數棧、方法出口等信息。
局部變量表裏存放了編譯期間可知的各類基本數據類型(8種)、對象引用、returnAddress類型(指向一條字節碼指令的地址)。64位長度的long和double類型佔用2個局部變量空間(Slot),其他數據類型只佔用一個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
棧的大小能夠固定也能夠動態擴展。當棧調用深度大於JVM所容許的範圍,會拋出StackOverflowError的錯誤。若是擴展時沒法申請到足夠的內存,會拋出OutOfMemoryError異常。
來一張看一下比較直觀吧。
3、本地方法棧(Native Method Stack)
與虛擬機棧相似,區別是虛擬機棧執行java方法,本地方法站執行native方法。在虛擬機規範中對本地方法棧中方法使用的語言、使用方法與數據結構沒有強制規定,所以虛擬機能夠自由實現它。本地方法棧能夠拋出StackOverflowError和OutOfMemoryError異常。不過這塊區域咱們不怎麼去關心。
4、Java堆
Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立,用來存放對象實例。是內存中最大的一塊區域。垃圾收集器(GC)在該區域回收不使用的對象的內存空間。可是並非全部的對象都在這保存,深刻理解java虛擬機中說道,隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量調換優化技術將會致使一些微妙的變化,全部的對象都分配在堆上也逐漸變得不那麼絕對了。
堆的大小能夠固定也能夠動態擴展,可經過-Xms(最小值)和-Xmx(最大值)參數設置,若是在堆中沒有內存完成實例分配,且堆也沒法在擴展時,會拋出OutOfMemoryError異常。
5、方法區
方法區也是全部線程共享。主要用於存儲類的信息、常量池、靜態變量、及時編譯器編譯後的代碼等數據。方法區邏輯上屬於堆的一部分。一般又叫「Non-Heap(非堆)」。
一個例子理解所有
爲了理解的比較深入,先給一個例子。經過例子講解印象更加深入吧,假設咱們在idea或者是任何IDE環境中定義了一個類。
有一個person類
public class Person{
int age;
String name;
Baby baby;
public void walk() {
System.out.println("我正在走路。。。。");
}
}
還有個Baby類
public class Baby{
String babyname;
int babyAge;
public void cry(){
System.out.println("我是孩子,我會哭");
}
}
最後是一個測試類Test
public class Test {
public static void main(String[] args) {
Person person = new Person();
person.name = "馮鼕鼕的IT技術棧";
person.age = 18;
person.walk();
Baby baby= new Baby();
baby.babyname = "馮XX";
System.out.println(baby.babyname);
person.baby = baby;
System.out.println(pserson.baby.cry);
}
}
好了有了上面的環境,接下來就開始分析這些代碼在運行時內存的變化。如今在咱們的IDE開始運行。
第一步,JVM去方法區尋找Test類的代碼信息,若是有直接調用,沒有的話使用類的加載機制把類加載進來。同時把靜態變量、靜態方法、常量加載進來。這裏加載的是(「馮鼕鼕的IT技術棧」,「馮XX」);這是由於字符串是常量,age中的18是基本類型。
第二步,jvm進入main方法,看到Person person=new Person()。首先分析Person這個類,一樣的尋找Person類的代碼信息,有就加載,沒有的話類加載機制加載進來。同時也加載靜態變量、靜態方法、常量(「我正在走路。。。」)
第三步,jvm接下來看到了person,person在main方法內部,於是是局部變量,存放在棧空間中。
第四步,jvm接下來看到了new Person()。new出的對象(實例),存放在堆空間中。
第五步,jvm接下來看到了「=」,把new Person的地址告訴person變量,person經過四字節的地址(十六進制),引用該實例。 是否是有點暈,彆着急,畫個圖看一下。
第六步,jvm看到person.name = "馮鼕鼕的IT技術棧";person經過引用new Person實例的name屬性,該name屬性經過地址指向常量池的"馮鼕鼕的IT技術棧"。
第七步,jvm看到person.age = 18; person的age屬性是基本數據類型,直接賦值。
第八步,jvm看到person.walk(); 調用實例的方法時,並不會在實例對象中生成一個新的方法,而是經過地址指向方法區中類信息的方法。走到這一步再看看圖怎麼變化的。
第九步,jvm看到Baby baby=new Baby().這個過程和Person person = new Person()同樣
第十步,jvm看到baby.babyname = "馮XX";這個過程也和person.name = "馮鼕鼕的IT技術棧";同樣。
第十一步,jvm看到person.baby = baby;把baby對象引用賦值給Person實例的baby屬性屬性。
好了,到了這一步,應該對jvm的內存結構有一個詳細的認識了。這就結束了,固然還沒,我說過這是對java8的內存結構的分析,因此還要解釋一下兩個名詞:永久代(PermGen)和元空間(Metaspace)。
首先是永久代:
咱們常見的 "java.lang.OutOfMemoryError: PermGen space "這個異常。這裏的 「PermGen space」其實指的就是方法區。不過方法區和「PermGen space」又有着本質的區別。前者是 JVM 的規範,然後者則是 JVM 規範的一種實現,而且只有 HotSpot 纔有 「PermGen space」。因爲方法區主要存儲類的相關信息,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。
而後是元空間
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制,但能夠經過如下參數來指定元空間的大小:
-XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。
-XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。
除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
-XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
能夠這樣說在Java8中對永久代進行了徹底刪除。