轉載原文出處:http://www.codeceo.com/article/jvm-memory-stack.htmlhtml
JAVA可以實現跨平臺的一個根本緣由,是定義了class文件的格式標準,凡是實現該標準的JVM都可以加載並解釋該class文件,據此也能夠知道,爲啥Java語言的執行速度比C/C++語言執行的速度要慢了,固然緣由確定不止這一個,如在JVM中沒有數據寄存器,指令集使用的是棧來保存中間數據…等,儘管Java的貢獻者們爲執行速度的提升想了各類辦法,如JIT、動態編譯器等,如下是Leetcode中一道題目用不一樣的語言實現時的執行性能對比圖…java
如下是JVM的一個基本架構圖,在這個基本架構圖中,棧有兩部份,Java線程棧以及本地方法棧,棧的概念與C/C++程序基本上都是一個概念,裏面存放的都是棧幀,一個棧幀表明的就是一個函數的調用,在棧幀裏面存放了函數的形參,函數的局部變量, 返回地址等,可是與C/C++的一個重要區別是,C/C++裏面有傳值以及傳址的區別,當傳的是一個對象時( 結構體也能夠當成對象,其實就是對象~,只不過裏面的方法默認都是public的,不信你能夠試試,在結構體中加一個函數,編譯器也不會報錯,程序依舊運行~~~),會將對象復到到棧中,而Java中只有基本類型纔是傳值的,其餘類型傳的都是引用,什麼是引用,學過C/C++的就把引用看成指針理解吧~~~,在這個基本架構圖中,能夠看出JVM還定義了一個本地方法棧,本地方法棧是爲Java調用本地方法【這些本地方法是由其餘語言編寫的】服務的算法
上面的圖中看到的是JVM中棧有兩個,可是堆只有一個,每個線程都有自已的線程棧【線程棧的大小能夠經過設置JVM的-xss參數進行配置,32位系統下,JDK5.0之後每一個線程堆棧大小爲1M,之前每一個線程堆棧大小爲256K】,線程棧裏面的數據屬於該線程私有,可是全部的線程都共享一個堆空間,堆中存放的是對象數據,什麼是對象數據,排除法,排除基本類型以及引用類型之外的數據都將放在堆空間中。其中方法區和堆是全部線程共享的數據區。架構
在CPU的寄存器中有一個PC寄存器,存放下一條指令地址,這裏,虛擬機不使用CPU的程序計數器,本身在內存中設立一片區域來模擬CPU的程序計數器。只有一個程序計數器是不夠的,當多個線程切換執行時,那就單個程序計數器就沒辦法了,虛擬機規範中指出,每一條線程都有一個獨立的程序計數器。注意,Java虛擬機中的程序計數器指向正在執行的字節碼地址,而不是下一條。app
Java虛擬機棧也是線程私有的,虛擬機棧描述的是Java方法執行的內存模型:每一個方法執行的時候都會建立一個棧幀,用於存放局部變量表,操做數棧,動態連接,方法出口等信息。每個方法從調用直到執行完成的過程都對應着一個棧幀在虛擬機中的入棧到出棧的過程。咱們平時把內存分爲堆內存和棧內存,其中的棧內存就指的是虛擬機棧的局部變量表部分。局部變量表存放了編譯期能夠知道的基本數據類型(boolean、byte、char、short、int、float、long、double),對象引用(多是一個指向對象起始地址的引用指針,也可能指向一個表明對象的句柄或者其餘與此對象相關的位置),和返回後所指向的字節碼的地址。其中64 位長度的long 和double 類型的數據會佔用2個局部變量空間(Slot),其他的數據類型只佔用1個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。當遞歸層次太深時,會引起java.lang.StackOverflowError,這是虛擬機棧拋出的異常。jvm
在HotSpot虛擬機將本地方法棧和虛擬機棧合二爲一,它們的區別在於,虛擬機棧爲執行Java方法服務,而本地方法棧則爲虛擬機使用到的Native方法服務。xss
Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。這個區域是用來存放對象實例的,幾乎全部對象實例都會在這裏分配內存。堆是Java垃圾收集器管理的主要區域(GC堆),垃圾收集器實現了對象的自動銷燬。Java堆能夠細分爲:新生代和老年代;再細緻一點的有Eden空間,From Survivor空間,To Survivor空間等。Java堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,就像咱們的磁盤空間同樣。能夠經過-Xmx和-Xms控制函數
方法區也叫永久代。在過去(自定義類加載器還不是很常見的時候),類大可能是」static」的,不多被卸載或收集,所以被稱爲「永久的(Permanent)」。雖然Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java 堆區分開來。同時,因爲類class是JVM實現的一部分,並非由應用建立的,因此又被認爲是「非堆(non-heap)」內存。HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對於其餘虛擬機(如BEA JRockit、IBM J9 等)來講是不存在永久代的概念的。佈局
永久代也是各個線程共享的區域,它用於存儲已經被虛擬機加載過的類信息,常量,靜態變量(JDK7中被移到Java堆),即時編譯期編譯後的代碼(類方法)等數據。這裏要講一下運行時常量池,它是方法區的一部分,用於存放編譯期生成的各類字面量和符號引用(其實就是八大基本類型的包裝類型和String類型數據(JDK7中被移到Java堆))(官方文檔說明: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application)。性能
在JDK1.7中的HotASpot中,已經把本來放在方法區的字符串常量池移出。
從JDK7開始永久代的移除工做,貯存在永久代的一部分數據已經轉移到了Java Heap或者是Native Heap。但永久代仍然存在於JDK7,並無徹底的移除:符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。隨着JDK8的到來,JVM再也不有PermGen。但類的元數據信息(metadata)還在,只不過再也不是存儲在連續的堆空間上,而是移動到叫作「Metaspace」的本地內存(Native memory)中。
在JVM中共享數據空間劃分以下圖所示
上圖中,刻畫了Java程序運行時的堆空間,能夠簡述成以下2條
1.JVM中共享數據空間能夠分紅三個大區,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分爲新生代和老年代
2.新生代能夠劃分爲三個區,Eden區(存放新生對象),兩個倖存區(From Survivor和To Survivor)(存放每次垃圾回收後存活的對象)
3.永久代管理class文件、靜態對象、屬性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )
4.JVM垃圾回收機制採用「分代收集」:新生代採用複製算法,老年代採用標記清理算法。
做爲操做系統進程,Java 運行時面臨着與其餘進程徹底相同的內存限制:操做系統架構提供的可尋址地址空間和用戶空間。
操 做系統架構提供的可尋址地址空間,由處理器的位數決定,32 位提供了 2^32 的可尋址範圍,也就是 4,294,967,296 位,或者說 4GB。而 64 位處理器的可尋址範圍明顯增大:2^64,也就是 18,446,744,073,709,551,616,或者說 16 exabyte(百億億字節)。
地址空間被劃分爲用戶空間和內核空間。內核是主要的操做系統程序和C運行時,包含用於鏈接計算機硬件、調度程序以及提供聯網和虛擬內存等服務的邏輯和基於C的進程(JVM)。除去內核空間就是用戶空間,用戶空間纔是 Java 進程實際運行時使用的內存。
默認狀況下,32 位 Windows 擁有 2GB 用戶空間和 2GB 內核空間。在一些 Windows 版本上,經過向啓動配置添加 /3GB 開關並使用 /LARGEADDRESSAWARE 開關從新連接應用程序,能夠將這種平衡調整爲 3GB 用戶空間和 1GB 內核空間。在 32 位 Linux 上,默認設置爲 3GB 用戶空間和 1GB 內核空間。一些 Linux 分發版提供了一個hugemem內核,支持 4GB 用戶空間。爲了實現這種配置,將進行系統調用時使用的地址空間分配給內核。經過這種方式增長用戶空間會減慢系統調用,由於每次進行系統調用時,操做系統必須在地址空間之間複製數據並重置進程地址-空間映射。
下圖爲一個32 位 Java 進程的內存佈局:
可尋址的地址空間總共有 4GB,OS 和 C 運行時大約佔用了其中的 1GB,Java 堆佔用了將近 2GB,本機堆佔用了其餘部分。請注意,JVM 自己也要佔用內存,就像 OS 內核和 C 運行時同樣。
注意:
1. 上文提到的可尋址空間即指最大地址空間。
2. 對於2GB的用戶空間,理論上Java堆內存最大爲1.75G,但一旦Java線程的堆達到1.75G,那麼就會出現本地堆的Out-Of-Memory錯誤,因此實際上Java堆的最大可以使用內存爲1.5G。
在JVM運行時,能夠經過配置如下參數改變整個JVM堆的配置比例
1.Java heap的大小(新生代+老年代) -Xms堆的最小值 -Xmx堆空間的最大值 2.新生代堆空間大小調整 -XX:NewSize新生代的最小值 -XX:MaxNewSize新生代的最大值 -XX:NewRatio設置新生代與老年代在堆空間的大小 -XX:SurvivorRatio新生代中Eden所佔區域的大小 3.永久代大小調整 -XX:MaxPermSize 4.其餘 -XX:MaxTenuringThreshold,設置將新生代對象轉到老年代時須要通過多少次垃圾回收,可是仍然沒有被回收
在上面的配置中,老年代所佔空間的大小是由-XX:SurvivorRatio這個參數進行配置的,看完了上面的JVM堆空間分配圖,可能會奇怪,爲啥新生代空間要劃分爲三個區Eden及兩個Survivor區?有何用意?爲何要這麼分?要理解這個問題,就得理解一下JVM的垃圾收集機制(複製算法也叫copy算法),步驟以下:
複製(Copying)算法
將內存平均分紅A、B兩塊,算法過程:
1. 新生對象被分配到A塊中未使用的內存當中。當A塊的內存用完了, 把A塊的存活對象對象複製到B塊。
2. 清理A塊全部對象。
3. 新生對象被分配的B塊中未使用的內存當中。當B塊的內存用完了, 把B塊的存活對象對象複製到A塊。
4. 清理B塊全部對象。
5. goto 1。
優勢:簡單高效。缺點:內存代價高,有效內存爲佔用內存的一半。
圖解說明以下所示:(圖中後觀是一個循環過程)
對複製算法進一步優化:使用Eden/S0/S1三個分區
平均分紅A/B塊太浪費內存,採用Eden/S0/S1三個區更合理,空間比例爲Eden:S0:S1==8:1:1,有效內存(便可分配新生對象的內存)是總內存的9/10。
算法過程:
1. Eden+S0可分配新生對象;
2. 對Eden+S0進行垃圾收集,存活對象複製到S1。清理Eden+S0。一次新生代GC結束。
3. Eden+S1可分配新生對象;
4. 對Eden+S1進行垃圾收集,存活對象複製到S0。清理Eden+S1。二次新生代GC結束。
5. goto 1。
默認Eden:S0:S1=8:1:1,所以,新生代中可使用的內存空間大小佔用新生代的9/10,那麼有人就會問,爲何不直接分紅兩個區,一個區佔9/10,另外一個區佔1/10,這樣作的緣由大概有如下幾種
1.S0與S1的區間明顯較小,有效新生代空間爲Eden+S0/S1,所以有效空間就大,增長了內存使用率
2.有利於對象代的計算,當一個對象在S0/S1中達到設置的XX:MaxTenuringThreshold值後,會將其分到老年代中,設想一下,若是沒有S0/S1,直接分紅兩個區,該如何計算對象通過了多少次GC還沒被釋放,你可能會說,在對象里加一個計數器記錄通過的GC次數,或者存在一張映射表記錄對象和GC次數的關係,是的,能夠,可是這樣的話,會掃描整個新生代中的對象, 有了S0/S1咱們就能夠只掃描S0/S1區了~~~