java中棧內存與堆內存(JVM內存模型)
Java中堆內存和棧內存詳解1 和 Java中堆內存和棧內存詳解2 都粗略講解了棧內存和堆內存的區別,以及代碼中哪些變量存儲在堆中、哪些存儲在棧中。內存中的堆和棧到底是什么 詳細講述了程序在內存中的模型,從可執行文件(ELF)格式的編譯介紹了堆和棧,主要是C/C++語言,講的比較清楚,借鑒性比較強。
其實,對於java語言,編譯后的文件是一個中間字節代碼,操作系統不能直接執行,需要jvm解釋執行。與C/C++對應起來,理解java的棧內存和堆內存,應該從jvm的內存模型入手(參考深入理解JVM-內存模型(jmm)和GC)。
一、Java內存模型
java程序內存的分配是在JVM虛擬機內存分配機制下完成。
java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規范的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平台下對內存的訪問都能保證效果一致的機制及規范。
根據java虛擬機規范,java虛擬機管理的內存將分為下面五大區域。
1.程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現,也就是說,在同一時刻一個處理器內核只會執行一條線程,處理器切換線程時並不會記錄上一個線程執行到哪個位置,所以為了線程切換后依然能恢復到原位,每條線程都需要有各自獨立的程序計數器。
特點:
-
線程私有
-
JVM規范中唯一沒有規定OutOfMemoryError情況的區域
-
如果正在執行的是Native 方法,則這個計數器值為空
2. java棧(虛擬機棧)(具體參考JVM 系列 - 內存區域 - Java 虛擬機棧(三))
-
Java 虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命周期隨着線程,線程啟動而產生,線程結束而消亡。
-
Java 虛擬機棧描述的是 Java 方法執行的內存模型,用於存儲棧幀。線程啟動時會創建虛擬機棧,每個方法在執行時會在虛擬機棧中創建一個棧幀,用於存儲局部變量表、操作數棧、動態連接、方法返回地址、附加信息等信息。每個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中的入棧(壓棧)到出棧(彈棧)的過程。
-
Java 虛擬機棧使用的內存不需要保證是連續的。
-
Java 虛擬機規范即允許 Java 虛擬機棧被實現成固定大小(-Xss),也允許通過計算結果動態來擴容和收縮大小。如果采用固定大小的 Java 虛擬機棧,那每個線程的 Java 虛擬機棧容量可以在線程創建的時候就已經確定。
Java 虛擬機棧中的單位元素是棧幀,每個線程中調用同一個方法或者不同的方法,都會創建不同的棧幀。在 Running 的線程,只有當前棧幀有效(Java 虛擬機棧中棧頂的棧幀),與當前棧幀相關聯的方法稱為當前方法。每調用一個新的方法,被調用方法對應的棧幀就會被放到棧頂(入棧),也就是成為新的當前棧幀。當一個方法執行完成退出的時候,此方法對應的棧幀也相應銷毀(出棧)。
每個棧幀中存放局部變量表、操作數棧、動態鏈接、方法返回地址、附加信息。
3. 本地方法棧
本地方法棧(Native Method Stacks)與 Java 虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的 Native 方法服務。虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。
Navtive 方法是 Java 通過 JNI 直接調用本地 C/C++ 庫,可以認為是 Native 方法相當於 C/C++ 暴露給 Java 的一個接口,Java 通過調用這個接口從而調用到 C/C++ 方法。當線程調用 Java 方法時,虛擬機會創建一個棧幀並壓入 Java 虛擬機棧。然而當它調用的是 native 方法時,虛擬機會保持 Java 虛擬機棧不變,也不會向 Java 虛擬機棧中壓入新的棧幀,虛擬機只是簡單地動態連接並直接調用指定的 native 方法。
4.堆(參考JVM 系列 - 內存區域 - Java 堆(五))
Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊,也被稱為 "GC堆",是被所有線程共享的一塊內存區域,在虛擬機啟動時被創建。
唯一目的就是儲存對象實例和數組(JDK7 已把字符串常量池和類靜態變量移動到 Java 堆),幾乎所有的對象實例都會存儲在堆中分配。隨着 JIT 編譯器發展,逃逸分析、棧上分配、標量替換等優化技術導致並不是所有對象都會在堆上分配。
Java 堆是垃圾收集器管理的主要區域。堆內存分為新生代 (Young) 和老年代 (Old) ,新生代 (Young) 又被划分為三個區域:Eden、From Survivor、To Survivor。
根據 Java 虛擬機規范的規定,Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過 -Xmx 和 -Xms 控制)。
5.方法區(https://www.jianshu.com/p/59f98076b382)
方法區(Method Area)與 Java 堆一樣,是所有線程共享的內存區域。
JDK7 之前(永久代)用於存儲已被虛擬機加載的類信息、常量、字符串常量、類靜態變量、即時編譯器編譯后的代碼等數據。
Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將類在加載后進入方法區的運行時常量池中存放。運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的是 String.intern() 方法。
java 中基本類型的包裝類的大部分都實現了常量池技術,這些類是 Byte、Short、Integer、Long、Character、Boolean,另外 Float 和 Double 類型的包裝類則沒有實現。另外 Byte、Short、Integer、Long、Character 這5種整型的包裝類也只是在對應值在-128到127之間時才可使用對象池。 |
在老版jdk,方法區也被稱為永久代(可以通過 -XX:PermSize 和 -XX:MaxPermSize 來進行調節大小),JDK8 徹底將永久代移除出 HotSpot JVM,將其原有的數據遷移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一個內存區域被稱為元空間(Metaspace)。
元空間(Metaspace):元空間是方法區的在 HotSpot JVM 中的實現,方法區主要用於存儲類信息、常量池、方法數據、方法代碼、符號引用等。元空間的本質和永久代類似,都是對 JVM 規范中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。元空間的大小理論上取決於32位/64位系統內存大小,可以通過 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置內存大小。
二、常說的java中的棧內存和堆內存
我們經常說的棧內存和堆內存只是java內存模型中的一部分內容,也就是編程過程中關注比較多的部分。
通常說的棧一般指棧幀中的局部變量表(存放的8種類型: byte、short、int、long、float、double、char、boolean和reference、returnAddress),它是一片連續的內存空間,用來存放方法參數,以及方法內定義的局部變量,存放着編譯期間已知的數據類型。局部變量表所需要的內存空間在編譯期完成分配,當進入一個方法時,這個方法在棧中需要分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表大小。
通常說的堆一般指java內存模型中的堆,用於儲存對象實例和數組,幾乎所有的對象實例都會存儲在堆中分配。java堆是java虛擬機管理的內存中最大的一塊,也被稱為 "GC堆",是垃圾收集器管理的主要區域。
三、棧內存和堆內存的區別(只是便於記憶,並不嚴謹)
-
存儲的數據及生命周期
棧主要用於存儲方法參數、局部變量和對象的引用變量,存放的是編譯期間已知的數據類型(八大基本類型和對象引用(reference類型),returnAddress類型。每個線程都會有一個獨立的棧空間,棧內存的數據生命周期隨線程的結束而結束。
所有對象實例及數組都要在堆上分配內存,堆存放的對象是線程共享的,線程結束時,對象實例和數組的生命周期並不一定結束,只有被GC回收后生命周期結束。
-
空間大小及限制
棧的內存大小在編譯時確定,是一段連續的空間,運行時不會改變,棧內存隨線程的結束自動回收。如果請求的棧的深度大於虛擬機允許的棧深度,JVM會拋出java.lang.StackOverFlowError。
堆內存在程序運行時動態分配,可以是存在物理上不連續的內存空間,線程運行結束后GC進行回收(只有對象或數組不再被引用時才回收)。如果是堆內存沒有可用的空間存儲生成的對象,JVM會拋出java.lang.OutOfMemoryError。
-
獨占或共享
棧內存歸屬於單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。而堆內存中的對象對所有線程可見。堆內存中的對象可以被所有線程訪問。
-
分配效率
棧由系統自動分配,速度較快。堆由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。
-
存取速度(jvm中可能會不同)
由於很多CPU對壓棧、出棧操作有硬件(指令)上的支持,所以在棧區分配/歸還內存速度極快(相比之下,堆上分配簡直是龜速);尤其是函數內部的局部變量,可以輕易與函數調用/返回綁定,因此幾乎所有編譯型語言都會在利用棧管理局部變量(而且會優先使用空閑的寄存器,所以幾乎所有高級語言都是訪問局部變量速度最快)。