深入了解JVM(Java虛擬機)內(nèi)存結構
JVM內(nèi)存結構
Java虛擬機的內(nèi)存結構分為5個部分:
- 程序計數(shù)器
- Java 虛擬機棧
- 本地方法棧
- 堆
- 方法區(qū)
JDK1.7與1.8的區(qū)別:
- 1.7中有有一個永久代,存儲的是類信息、靜態(tài)變量、常量、編譯后的代碼
- 1.8移除了永久代,把數(shù)據(jù)存儲到了本地內(nèi)存的元空間中,防止內(nèi)存溢出
程序計數(shù)器(PC寄存器)
程序計數(shù)器定義
線程私有的,每個線程一份,內(nèi)部保存的字節(jié)碼的行號。用于記錄正在執(zhí)行的字節(jié)碼指令的地址。
程序計數(shù)器是一塊較小的內(nèi)存空間,是當前線程正在執(zhí)行的那條字節(jié)碼指令的地址。若當前線程正在執(zhí)行的是一個本地方法,那么此時程序計數(shù)器為 Undefined
。
javap -v xx.class 打印堆棧大小,局部變量的數(shù)量和方法的參數(shù)。
程序計數(shù)器的作用
- 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令,從而實現(xiàn)代碼的流程控制。
- 在多線程情況下,程序計數(shù)器記錄的是當前線程執(zhí)行的位置,從而當線程切換回來時,就知道上次線程執(zhí)行到哪了。
程序計數(shù)器的特點
- 是一塊較小的內(nèi)存空間。
- 線程私有,每條線程都有自己的程序計數(shù)器。
- 生命周期:隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結束而銷毀。
- 是唯一一個不會出現(xiàn)
OutOfMemoryError
的內(nèi)存區(qū)域。
Java虛擬機棧
Java虛擬機棧的定義
Java 虛擬機棧是描述 Java 方法運行過程的內(nèi)存模型。
Java Virtual machine Stacks (java 虛擬機棧)
- 每個線程運行時所需要的內(nèi)存,稱為虛擬機棧,先進后出
- 每個棧由多個棧幀(frame)組成,對應著每次方法調(diào)用時所占用的內(nèi)存
- 每個線程只能有一個活動棧幀,對應著當前正在執(zhí)行的那個方法
垃圾回收是否涉及棧內(nèi)存?
垃圾回收主要指就是堆內(nèi)存,當棧幀彈棧以后,內(nèi)存就會釋放
棧內(nèi)存分配越大越好嗎?
未必,默認的棧內(nèi)存通常為1024k
棧幀過大會導致線程數(shù)變少,例如,機器總內(nèi)存為512m,目前能活動的線程數(shù)則為512個,如果把棧內(nèi)存改為2048k,那么能活動的棧幀就會減半
方法內(nèi)的局部變量是否線程安全?
- 如果方法內(nèi)局部變量沒有逃離方法的作用范圍,它是線程安全的
- 如果是局部變量引用了對象,并逃離方法的作用范圍,需要考慮線程安全
虛擬機棧的組成
Java 虛擬機棧會為每一個即將運行的 Java 方法創(chuàng)建一塊叫做“棧幀”的區(qū)域,用于存放該方法運行過程中的一些信息,如:
- 局部變量表
- 操作數(shù)棧
- 動態(tài)鏈接
- 方法出口信息
- ......
壓棧出棧過程
當方法運行過程中需要創(chuàng)建局部變量時,就將局部變量的值存入棧幀中的局部變量表中。
Java 虛擬機棧的棧頂?shù)臈钱斍罢趫?zhí)行的活動棧,也就是當前正在執(zhí)行的方法,PC 寄存器也會指向這個地址。只有這個活動的棧幀的本地變量可以被操作數(shù)棧使用,當在這個棧幀中調(diào)用另一個方法,與之對應的棧幀又會被創(chuàng)建,新創(chuàng)建的棧幀壓入棧頂,變?yōu)楫斍暗幕顒訔?/p>
方法結束后,當前棧幀被移出,棧幀的返回值變成新的活動棧幀中操作數(shù)棧的一個操作數(shù)。如果沒有返回值,那么新的活動棧幀中操作數(shù)棧的操作數(shù)沒有變化。
由于 Java 虛擬機棧是與線程對應的,數(shù)據(jù)不是線程共享的(也就是線程私有的),因此不用關心數(shù)據(jù)一致性問題,也不會存在同步鎖的問題。
局部變量表
定義為一個數(shù)字數(shù)組,主要用于存儲方法參數(shù)、定義在方法體內(nèi)部的局部變量,數(shù)據(jù)類型包括各類基本數(shù)據(jù)類型,對象引用,以及 return address 類型。
局部變量表容量大小是在編譯期確定下來的。最基本的存儲單元是 slot,32 位占用一個 slot,64 位類型(long 和 double)占用兩個 slot。
對于 slot 的理解:
- JVM 虛擬機會為局部變量表中的每個 slot 都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值。
- 如果當前幀是由構造方法或者實例方法創(chuàng)建的,那么該對象引用 this,會存放在 index 為 0 的 slot 處,其余的參數(shù)表順序繼續(xù)排列。
- 棧幀中的局部變量表中的槽位是可以重復的,如果一個局部變量過了其作用域,那么其作用域之后申明的新的局部變量就有可能會復用過期局部變量的槽位,從而達到節(jié)省資源的目的。
在棧幀中,與性能調(diào)優(yōu)關系最密切的部分,就是局部變量表,方法執(zhí)行時,虛擬機使用局部變量表完成方法的傳遞局部變量表中的變量也是重要的垃圾回收根節(jié)點,只要被局部變量表中直接或間接引用的對象都不會被回收。
操作數(shù)棧
- 棧頂緩存技術:由于操作數(shù)是存儲在內(nèi)存中,頻繁的進行內(nèi)存讀寫操作影響執(zhí)行速度,將棧頂元素全部緩存到物理 CPU 的寄存器中,以此降低對內(nèi)存的讀寫次數(shù),提升執(zhí)行引擎的執(zhí)行效率。
- 每一個操作數(shù)棧會擁有一個明確的棧深度,用于存儲數(shù)值,最大深度在編譯期就定義好。32bit 類型占用一個棧單位深度,64bit 類型占用兩個棧單位深度操作數(shù)棧。
- 并非采用訪問索引方式進行數(shù)據(jù)訪問,而是只能通過標準的入棧、出棧操作完成一次數(shù)據(jù)訪問。
本地方法棧
本地方法棧的定義
本地方法棧是為 JVM 運行 Native 方法準備的空間,由于很多 Native 方法都是用 C 語言實現(xiàn)的,所以它通常又叫 C 棧。它與 Java 虛擬機棧實現(xiàn)的功能類似,只不過本地方法棧是描述本地方法運行過程的內(nèi)存模型。
棧幀變化過程
本地方法被執(zhí)行時,在本地方法棧也會創(chuàng)建一塊棧幀,用于存放該方法的局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口信息等。
方法執(zhí)行結束后,相應的棧幀也會出棧,并釋放內(nèi)存空間。也會拋出 StackOverFlowError 和 OutOfMemoryError 異常。
如果 Java 虛擬機本身不支持 Native 方法,或是本身不依賴于傳統(tǒng)棧,那么可以不提供本地方法棧。如果支持本地方法棧,那么這個棧一般會在線程創(chuàng)建的時候按線程分配。
Java堆
堆的定義
線程共享的區(qū)域:主要用來保存對象實例,數(shù)組等,當堆中沒有內(nèi)存空間可分配給實例,也無法再擴展時,則拋出OutOfMemoryError異常。
堆是用來存放對象的內(nèi)存空間, 幾乎
所有的對象都存儲在堆中。
堆的特點
- 線程共享,整個 Java 虛擬機只有一個堆,所有的線程都訪問同一個堆。而程序計數(shù)器、Java 虛擬機棧、本地方法棧都是一個線程對應一個。
- 在虛擬機啟動時創(chuàng)建。
- 是垃圾回收的主要場所。
- 堆可分為新生代(Eden 區(qū):
From Survior
,To Survivor
)、老年代。 - Java 虛擬機規(guī)范規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應該被視為連續(xù)的。
- 關于 Survivor s0,s1 區(qū): 復制之后有交換,誰空誰是 to。
不同的區(qū)域存放不同生命周期的對象,這樣可以根據(jù)不同的區(qū)域使用不同的垃圾回收算法,更具有針對性。
堆的大小既可以固定也可以擴展,但對于主流的虛擬機,堆的大小是可擴展的,因此當線程請求分配內(nèi)存,但堆已滿,且內(nèi)存已無法再擴展時,就拋出 OutOfMemoryError 異常。
Java 堆所使用的內(nèi)存不需要保證是連續(xù)的。而由于堆是被所有線程共享的,所以對它的訪問需要注意同步問題,方法和對應的屬性都需要保證一致性。
新生代與老年代
- 老年代比新生代生命周期長。
- 新生代與老年代空間默認比例
1:2
:JVM 調(diào)參數(shù),XX:NewRatio=2
,表示新生代占 1,老年代占 2,新生代占整個堆的 1/3。 - HotSpot 中,Eden 空間和另外兩個 Survivor 空間缺省所占的比例是:
8:1:1
。 - 幾乎所有的 Java 對象都是在 Eden 區(qū)被 new 出來的,Eden 放不了的大對象,就直接進入老年代了。
對象分配過程
- new 的對象先放在 Eden 區(qū),大小有限制
- 如果創(chuàng)建新對象時,Eden 空間填滿了,就會觸發(fā) Minor GC,將 Eden 不再被其他對象引用的對象進行銷毀,再加載新的對象放到 Eden 區(qū),特別注意的是 Survivor 區(qū)滿了是不會觸發(fā) Minor GC 的,而是 Eden 空間填滿了,Minor GC 才順便清理 Survivor 區(qū)
- 將 Eden 中剩余的對象移到 Survivor0 區(qū)
- 再次觸發(fā)垃圾回收,此時上次 Survivor 下來的,放在 Survivor0 區(qū)的,如果沒有回收,就會放到 Survivor1 區(qū)
- 再次經(jīng)歷垃圾回收,又會將幸存者重新放回 Survivor0 區(qū),依次類推
- 默認是 15 次的循環(huán),超過 15 次,則會將幸存者區(qū)幸存下來的轉去老年區(qū) jvm 參數(shù)設置次數(shù) : -XX:MaxTenuringThreshold=N 進行設置
- 頻繁在新生區(qū)收集,很少在養(yǎng)老區(qū)收集,幾乎不在永久區(qū)/元空間搜集
Full GC /Major GC 觸發(fā)條件
- 顯示調(diào)用
System.gc()
,老年代的空間不夠,方法區(qū)的空間不夠等都會觸發(fā) Full GC,同時對新生代和老年代回收,F(xiàn)Ull GC 的 STW 的時間最長,應該要避免 - 在出現(xiàn) Major GC 之前,會先觸發(fā) Minor GC,如果老年代的空間還是不夠就會觸發(fā) Major GC,STW 的時間長于 Minor GC
逃逸分析
標量替換
- 標量不可在分解的量,java 的基本數(shù)據(jù)類型就是標量,標量的對立就是可以被進一步分解的量,而這種量稱之為聚合量。而在 JAVA 中對象就是可以被進一步分解的聚合量
- 替換過程,通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM 不會創(chuàng)建該對象,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。
- 對象和數(shù)組并非都是在堆上分配內(nèi)存的
- 《深入理解 Java 虛擬機中》關于 Java 堆內(nèi)存有這樣一段描述:隨著 JIT 編譯期的發(fā)展與逃逸分析技術逐漸成熟,
棧上分配
,標量替換
優(yōu)化技術將會導致一些變化,所有的對象都分配到堆上也漸漸變得不那么"絕對"了。 - 這是一種可以有效減少 Java 內(nèi)存堆分配壓力的分析算法,通過逃逸分析,Java Hotspot 編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
- 當一個對象在方法中被定義后,它可能被外部方法所引用,如作為調(diào)用參數(shù)傳遞到其他地方中,稱為
方法逃逸
。 - 再如賦值給類變量或可以在其他線程中訪問的實例變量,稱為
線程逃逸
- 使用逃逸分析,編譯器可以對代碼做如下優(yōu)化:
- 同步省略:如果一個對象被發(fā)現(xiàn)只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
- 將堆分配轉化為棧分配:如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
- 分離對象或標量替換:有的對象可能不需要作為一個連續(xù)的內(nèi)存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內(nèi)存,而是存儲在 CPU 寄存器中。
public?static?StringBuffer?createStringBuffer(String?s1,?String?s2)?{ ????StringBuffer?s?=?new?StringBuffer(); ????s.append(s1); ????s.append(s2); ????return?s; }
s 是一個方法內(nèi)部變量,上邊的代碼中直接將 s 返回,這個 StringBuffer 的對象有可能被其他方法所改變,導致它的作用域就不只是在方法內(nèi)部,即使它是一個局部變量,但還是逃逸到了方法外部,稱為 方法逃逸
。
還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為 線程逃逸
。
- 在編譯期間,如果 JIT 經(jīng)過逃逸分析,發(fā)現(xiàn)有些對象沒有逃逸出方法,那么有可能堆內(nèi)存分配會被優(yōu)化成棧內(nèi)存分配。
- jvm 參數(shù)設置,
-XX:+DoEscapeAnalysis
:開啟逃逸分析 ,-XX:-DoEscapeAnalysis
: 關閉逃逸分析 - 從 jdk 1.7 開始已經(jīng)默認開始逃逸分析。
TLAB
- TLAB 的全稱是 Thread Local Allocation Buffer,即線程本地分配緩存區(qū),是屬于 Eden 區(qū)的,這是一個線程專用的內(nèi)存分配區(qū)域,線程私有,默認開啟的(當然也不是絕對的,也要看哪種類型的虛擬機)
- 堆是全局共享的,在同一時間,可能會有多個線程在堆上申請空間,但每次的對象分配需要同步的進行(虛擬機采用 CAS 配上失敗重試的方式保證更新操作的原子性)但是效率卻有點下降
- 所以用 TLAB 來避免多線程沖突,在給對象分配內(nèi)存時,每個線程使用自己的 TLAB,這樣可以使得線程同步,提高了對象分配的效率
- 當然并不是所有的對象都可以在 TLAB 中分配內(nèi)存成功,如果失敗了就會使用加鎖的機制來保持操作的原子性
-XX:+UseTLAB
使用 TLAB,-XX:+TLABSize
設置 TLAB 大小
方法區(qū)
Java 虛擬機規(guī)范中定義方法區(qū)是堆的一個邏輯部分。方法區(qū)存放以下信息:
- 已經(jīng)被虛擬機加載的類信息
- 常量
- 靜態(tài)變量
- 即時編譯器編譯后的代碼
方法區(qū)的簡單理解:
- 方法區(qū)(Method Area)是各個線程共享的內(nèi)存區(qū)域
- 主要存儲類的信息、運行時常量池
- 虛擬機啟動的時候創(chuàng)建,關閉虛擬機時釋放
- 如果方法區(qū)域中的內(nèi)存無法滿足分配請求,則會拋出OutOfMemoryError: Metaspace
方法區(qū)的特點
- 線程共享。 方法區(qū)是堆的一個邏輯部分,因此和堆一樣,都是線程共享的。整個虛擬機中只有一個方法區(qū)。
- 永久代。 方法區(qū)中的信息一般需要長期存在,而且它又是堆的邏輯分區(qū),因此用堆的劃分方法,把方法區(qū)稱為“永久代”。
- 內(nèi)存回收效率低。 方法區(qū)中的信息一般需要長期存在,回收一遍之后可能只有少量信息無效。主要回收目標是:對常量池的回收;對類型的卸載。
- Java 虛擬機規(guī)范對方法區(qū)的要求比較寬松。 和堆一樣,允許固定大小,也允許動態(tài)擴展,還允許不實現(xiàn)垃圾回收。
運行時常量池
常量池可以看作是一張表,虛擬機指令根據(jù)這張常量表找到要執(zhí)行的類名、方法名、參數(shù)類型、字面量等信息。
常量池是 *.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量池,并把里面的符號地址變?yōu)檎鎸嵉刂?/p>
查看字節(jié)碼結構(類的基本信息、常量池、方法定義)
javap -v Application.class
直接內(nèi)存
直接內(nèi)存:并不屬于JVM中的內(nèi)存結構,不由JVM進行管理。是虛擬機的系統(tǒng)內(nèi)存,常見于 NIO 操作時,用于數(shù)據(jù)緩沖區(qū),它分配回收成本較高,但讀寫性能高。
直接內(nèi)存的大小不受 Java 虛擬機控制,但既然是內(nèi)存,當內(nèi)存不足時就會拋出 OutOfMemoryError 異常。
常規(guī)IO的數(shù)據(jù)拷貝流程
NIO數(shù)據(jù)拷貝流程
直接內(nèi)存與堆內(nèi)存比較
- 直接內(nèi)存申請空間耗費更高的性能
- 直接內(nèi)存讀取 IO 的性能要優(yōu)于普通的堆內(nèi)存
- 直接內(nèi)存作用鏈: 本地 IO -> 直接內(nèi)存 -> 本地 IO
- 堆內(nèi)存作用鏈:本地 IO -> 直接內(nèi)存 -> 非直接內(nèi)存 -> 直接內(nèi)存 -> 本地 IO
服務器管理員在配置虛擬機參數(shù)時,會根據(jù)實際內(nèi)存設置 -Xmx 等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制,從而導致動態(tài)擴展時出現(xiàn) OutOfMemoryError 異常。
以上就是深入了解JVM(Java虛擬機)內(nèi)存結構的詳細內(nèi)容,更多關于JVM內(nèi)存結構的資料請關注腳本之家其它相關文章!
相關文章
Netty分布式NioEventLoop任務隊列執(zhí)行源碼分析
這篇文章主要為大家介紹了Netty分布式NioEventLoop任務隊列執(zhí)行源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-03-03Maven項src/main/java目錄下配置文件無法被導出或者生效的問題和處理方案
這篇文章主要介紹了Maven項src/main/java目錄下配置文件無法被導出或者生效的問題和處理方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11Spring @Profile注解實現(xiàn)多環(huán)境配置
這篇文章主要介紹了Spring @Profile注解實現(xiàn)多環(huán)境配置,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04IDEA導入外部項目報Error:java: 無效的目標發(fā)行版: 11的解決方法
這篇文章主要介紹了IDEA導入外部項目報Error:java: 無效的目標發(fā)行版: 11,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09Java通過匿名類來實現(xiàn)回調(diào)函數(shù)實例總結
這篇文章主要介紹了Java通過匿名類來實現(xiàn)回調(diào)函數(shù)的例子,回調(diào)函數(shù)就是一種函數(shù)簽名(若干個輸入?yún)?shù)、一個輸出參數(shù))的規(guī)范,java雖不存在函數(shù)聲明,但是java可以用接口來強制規(guī)范。具體操作步驟大家可查看下文的詳細講解,感興趣的小伙伴們可以參考一下。2017-08-08