<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

JVM運行時數據區之堆內存

馬哥Linux運維 ? 來源:馬哥Linux運維 ? 2023-08-19 14:35 ? 次閱讀

閱讀前思考

說一下 JVM 運行時數據區吧,都有哪些區?分別是干什么的?

Java 8 的內存分代改進

舉例棧溢出的情況?

調整棧大小,就能保存不出現溢出嗎?

分配的棧內存越大越好嗎?

垃圾回收是否會涉及到虛擬機棧?

方法中定義的局部變量是否線程安全?

8fd8afc6-3dcb-11ee-ac96-dac502259ad0.png

運行時數據區

內存是非常重要的系統資源,是硬盤和 CPU 的中間倉庫及橋梁,承載著操作系統和應用程序的實時運行。JVM 內存布局規定了 Java 在運行過程中內存申請、分配、管理的策略,保證了 JVM 的高效穩定運行。不同的 JVM 對于內存的劃分方式和管理機制存在著部分差異。

下圖是 JVM 整體架構,中間部分就是 Java 虛擬機定義的各種運行時數據區域。

8ff31b54-3dcb-11ee-ac96-dac502259ad0.png

Java 虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨著虛擬機啟動而創建,隨著虛擬機退出而銷毀。另外一些則是與線程一一對應的,這些與線程一一對應的數據區域會隨著線程開始和結束而創建和銷毀。

線程私有:程序計數器、棧、本地棧

線程共享:堆、堆外內存(永久代或元空間、代碼緩存)

四、堆內存

內存劃分

對于大多數應用,Java 堆是 Java 虛擬機管理的內存中最大的一塊,被所有線程共享。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數據都在這里分配內存。

為了進行高效的垃圾回收,虛擬機把堆內存邏輯上劃分成三塊區域(分代的唯一理由就是優化 GC 性能):

新生帶(年輕代):新對象和沒達到一定年齡的對象都在新生代

老年代(養老區):被長時間使用的對象,老年代的內存空間應該要比年輕代更大

元空間(JDK1.8 之前叫永久代):像一些方法中的操作臨時對象等,JDK1.8 之前是占用 JVM 內存,JDK1.8 之后直接使用物理內存

901e2cb8-3dcb-11ee-ac96-dac502259ad0.png

Java 虛擬機規范規定,Java 堆可以是處于物理上不連續的內存空間中,只要邏輯上是連續的即可,像磁盤空間一樣。實現時,既可以是固定大小,也可以是可擴展的,主流虛擬機都是可擴展的(通過-Xmx和-Xms控制),如果堆中沒有完成實例分配,并且堆無法再擴展時,就會拋出OutOfMemoryError異常。

年輕代 (Young Generation)

年輕代是所有新對象創建的地方。當填充年輕代時,執行垃圾收集。這種垃圾收集稱為Minor GC。年輕一代被分為三個部分——伊甸園(Eden Memory)和兩個幸存區(Survivor Memory,被稱為from/to或s0/s1),默認比例是81

大多數新創建的對象都位于 Eden 內存空間中

當 Eden 空間被對象填充時,執行Minor GC,并將所有幸存者對象移動到一個幸存者空間中

Minor GC 檢查幸存者對象,并將它們移動到另一個幸存者空間。所以每次,一個幸存者空間總是空的

經過多次 GC 循環后存活下來的對象被移動到老年代。通常,這是通過設置年輕一代對象的年齡閾值來實現的,然后他們才有資格提升到老一代

老年代(Old Generation)

舊的一代內存包含那些經過許多輪小型 GC 后仍然存活的對象。通常,垃圾收集是在老年代內存滿時執行的。老年代垃圾收集稱為 主GC(Major GC),通常需要更長的時間。

大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在 Eden 區和兩個Survivor 區之間發生大量的內存拷貝

90356ae0-3dcb-11ee-ac96-dac502259ad0.png

元空間

不管是 JDK8 之前的永久代,還是 JDK8 及以后的元空間,都可以看作是 Java 虛擬機規范中方法區的實現。

雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開。

所以元空間放在后邊的方法區再說。

904e4254-3dcb-11ee-ac96-dac502259ad0.png

設置堆內存大小和 OOM

Java 堆用于存儲 Java 對象實例,那么堆的大小在 JVM 啟動的時候就確定了,我們可以通過-Xmx和-Xms來設定

-Xmx用來表示堆的起始內存,等價于-XX:InitialHeapSize

-Xms用來表示堆的最大內存,等價于-XX:MaxHeapSize

如果堆的內存大小超過-Xms設定的最大內存, 就會拋出OutOfMemoryError異常。

我們通常會將-Xmx和-Xms兩個參數配置為相同的值,其目的是為了能夠在垃圾回收機制清理完堆區后不再需要重新分隔計算堆的大小,從而提高性能

默認情況下,初始堆內存大小為:電腦內存大小/64

默認情況下,最大堆內存大小為:電腦內存大小/4

可以通過代碼獲取到我們的設置值,當然也可以模擬 OOM:

public static void main(String[] args) {


  //返回 JVM 堆大小
  long initalMemory = Runtime.getRuntime().totalMemory() / 1024 /1024;
  //返回 JVM 堆的最大內存
  long maxMemory = Runtime.getRuntime().maxMemory() / 1024 /1024;


  System.out.println("-Xms : "+initalMemory + "M");
  System.out.println("-Xmx : "+maxMemory + "M");


  System.out.println("系統內存大?。? + initalMemory * 64 / 1024 + "G");
  System.out.println("系統內存大?。? + maxMemory * 4 / 1024 + "G");
}

查看 JVM 堆內存分配

在默認不配置 JVM 堆內存大小的情況下,JVM 根據默認值來配置當前內存大小

默認情況下新生代和老年代的比例是 1:2,可以通過–XX:NewRatio來配置

新生代中的Eden:From Survivor:To Survivor的比例是81,可以通過-XX:SurvivorRatio來配置

若在 JDK 7 中開啟了-XX:+UseAdaptiveSizePolicy,JVM 會動態調整 JVM 堆中各個區域的大小以及進入老年代的年齡

此時–XX:NewRatio和-XX:SurvivorRatio將會失效,而 JDK 8 是默認開啟-XX:+UseAdaptiveSizePolicy

在 JDK 8中,不要隨意關閉-XX:+UseAdaptiveSizePolicy,除非對堆內存的劃分有明確的規劃

每次 GC 后都會重新計算 Eden、From Survivor、To Survivor 的大小

計算依據是GC過程中統計的GC時間、吞吐量、內存占用量

java -XX:+PrintFlagsFinal -version | grep HeapSize
    uintx ErgoHeapSizeLimit                         = 0                                   {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
    uintx InitialHeapSize                          := 134217728                           {product}
    uintx LargePageHeapSizeThreshold                = 134217728                           {product}
    uintx MaxHeapSize                              := 2147483648                          {product}
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
 jmap -heap 進程號

對象在堆中的生命周期

在 JVM 內存模型的堆中,堆被劃分為新生代和老年代

新生代又被進一步劃分為Eden區Survivor區,Survivor 區由From SurvivorTo Survivor組成

當創建一個對象時,對象會被優先分配到新生代的 Eden 區

此時 JVM 會給對象定義一個對象年輕計數器(-XX:MaxTenuringThreshold)

當 Eden 空間不足時,JVM 將執行新生代的垃圾回收(Minor GC)

JVM 會把存活的對象轉移到 Survivor 中,并且對象年齡 +1

對象在 Survivor 中同樣也會經歷 Minor GC,每經歷一次 Minor GC,對象年齡都會+1

如果分配的對象超過了-XX:PetenureSizeThreshold,對象會直接被分配到老年代

對象的分配過程

為對象分配內存是一件非常嚴謹和復雜的任務,JVM 的設計者們不僅需要考慮內存如何分配、在哪里分配等問題,并且由于內存分配算法和內存回收算法密切相關,所以還需要考慮 GC 執行完內存回收后是否會在內存空間中產生內存碎片。

906c6310-3dcb-11ee-ac96-dac502259ad0.png

new 的對象先放在伊甸園區,此區有大小限制

當伊甸園的空間填滿時,程序又需要創建對象,JVM 的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區

然后將伊甸園中的剩余對象移動到幸存者 0 區

如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者 0 區,如果沒有回收,就會放到幸存者 1 區

如果再次經歷垃圾回收,此時會重新放回幸存者 0 區,接著再去幸存者 1 區

什么時候才會去養老區呢?默認是 15 次回收標記

在養老區,相對悠閑。當養老區內存不足時,再次觸發 Major GC,進行養老區的內存清理

若養老區執行了 Major GC 之后發現依然無法進行對象的保存,就會產生 OOM 異常

GC 垃圾回收簡介

Minor GC、Major GC、Full GC

JVM 在進行 GC 時,并非每次都對堆內存(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代。

針對 HotSpot VM 的實現,它里面的 GC 按照回收區域又分為兩大類:部分收集(Partial GC),整堆收集(Full GC)

部分收集:不是完整收集整個 Java 堆的垃圾收集。其中又分為:

目前只有 G1 GC 會有這種行為

目前,只有 CMS GC 會有單獨收集老年代的行為

很多時候 Major GC 會和 Full GC 混合使用,需要具體分辨是老年代回收還是整堆回收

新生代收集(Minor GC/Young GC):只是新生代的垃圾收集

老年代收集(Major GC/Old GC):只是老年代的垃圾收集

混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集

整堆收集(Full GC):收集整個 Java 堆和方法區的垃圾

TLAB

什么是 TLAB (Thread Local Allocation Buffer)?

從內存模型而不是垃圾回收的角度,對 Eden 區域繼續進行劃分,JVM 為每個線程分配了一個私有緩存區域,它包含在 Eden 空間內

多線程同時分配內存時,使用 TLAB 可以避免一系列的非線程安全問題,同時還能提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱為快速分配策略

OpenJDK 衍生出來的 JVM 大都提供了 TLAB 設計

為什么要有 TLAB ?

堆區是線程共享的,任何線程都可以訪問到堆區中的共享數據

由于對象實例的創建在 JVM 中非常頻繁,因此在并發環境下從堆區中劃分內存空間是線程不安全的

為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度

盡管不是所有的對象實例都能夠在 TLAB 中成功分配內存,但 JVM 確實是將 TLAB 作為內存分配的首選。

在程序中,可以通過-XX:UseTLAB設置是否開啟 TLAB 空間。

默認情況下,TLAB 空間的內存非常小,僅占有整個 Eden 空間的 1%,我們可以通過-XX:TLABWasteTargetPercent設置 TLAB 空間所占用 Eden 空間的百分比大小。

一旦對象在 TLAB 空間分配內存失敗時,JVM 就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在 Eden 空間中分配內存。

堆是分配對象存儲的唯一選擇嗎

隨著 JIT 編譯期的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了?!渡钊肜斫?Java 虛擬機》

逃逸分析

逃逸分析(Escape Analysis)是目前 Java 虛擬機中比較前沿的優化技術。這是一種可以有效減少 Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot 編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。

逃逸分析的基本行為就是分析對象動態作用域:

當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。

當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中,稱為方法逃逸。

例如:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb;
}

StringBuffer sb是一個方法內部變量,上述代碼中直接將sb返回,這樣這個 StringBuffer 有可能被其他方法所改變,這樣它的作用域就不只是在方法內部,雖然它是一個局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。

上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:

public static String createStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb.toString();
}

不直接返回 StringBuffer,那么 StringBuffer 將不會逃逸出方法。

參數設置:

在 JDK 6u23 版本之后,HotSpot 中默認就已經開啟了逃逸分析

如果使用較早版本,可以通過-XX"+DoEscapeAnalysis顯式開啟

開發中使用局部變量,就不要在方法外定義。

使用逃逸分析,編譯器可以對代碼做優化:

棧上分配:將堆分配轉化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配

同步省略:如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步

分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而存儲在 CPU 寄存器

JIT 編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象并沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成后,繼續在調用棧內執行,最后線程結束,??臻g被回收,局部變量對象也被回收。這樣就無需進行垃圾回收了。

常見棧上分配的場景:成員變量賦值、方法返回值、實例引用傳遞

代碼優化之同步省略(消除)

線程同步的代價是相當高的,同步的后果是降低并發性和性能

在動態編譯同步塊的時候,JIT 編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否能夠被一個線程訪問而沒有被發布到其他線程。如果沒有,那么 JIT 編譯器在編譯這個同步塊的時候就會取消對這個代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫做同步省略,也叫鎖消除。

public void keep() {
  Object keeper = new Object();
  synchronized(keeper) {
    System.out.println(keeper);
  }
}

如上代碼,代碼中對 keeper 這個對象進行加鎖,但是 keeper 對象的生命周期只在keep()方法中,并不會被其他線程所訪問到,所以在 JIT編譯階段就會被優化掉。優化成:

public void keep() {
  Object keeper = new Object();
  System.out.println(keeper);
}

代碼優化之標量替換

標量(Scalar)是指一個無法再分解成更小的數據的數據。Java 中的原始數據類型就是標量。

相對的,那些的還可以分解的數據叫做聚合(Aggregate),Java 中的對象就是聚合量,因為其還可以分解成其他聚合量和標量。

在 JIT 階段,通過逃逸分析確定該對象不會被外部訪問,并且對象可以被進一步分解時,JVM 不會創建該對象,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。這個過程就是標量替換。

通過-XX:+EliminateAllocations可以開啟標量替換,-XX:+PrintEliminateAllocations查看標量替換情況。

public static void main(String[] args) {
   alloc();
}


private static void alloc() {
   Point point = new Point(1,2);
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}

以上代碼中,point 對象并沒有逃逸出alloc()方法,并且 point 對象是可以拆解成標量的。那么,JIT 就不會直接創建 Point 對象,而是直接使用兩個標量 int x ,int y 來替代 Point 對象。

private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}

代碼優化之棧上分配

我們通過 JVM 內存分配可以知道 JAVA 中的對象都是在堆上進行分配,當對象沒有被引用的時候,需要依靠 GC 進行回收內存,如果對象數量較多的時候,會給 GC 帶來較大壓力,也間接影響了應用的性能。為了減少臨時對象在堆內分配的數量,JVM 通過逃逸分析確定該對象不會被外部訪問。那就通過標量替換將該對象分解在棧上分配內存,這樣該對象所占用的內存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。

總結:

關于逃逸分析的論文在1999年就已經發表了,但直到JDK 1.6才有實現,而且這項技術到如今也并不是十分成熟的。

其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程。

一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。

雖然這項技術并不十分成熟,但是他也是即時編譯器優化技術中一個十分重要的手段。

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • cpu
    cpu
    +關注

    關注

    68

    文章

    10481

    瀏覽量

    206943
  • 內存
    +關注

    關注

    8

    文章

    2780

    瀏覽量

    72853
  • JVM
    JVM
    +關注

    關注

    0

    文章

    152

    瀏覽量

    12134
  • 虛擬機
    +關注

    關注

    1

    文章

    860

    瀏覽量

    27451
  • 線程
    +關注

    關注

    0

    文章

    494

    瀏覽量

    19516

原文標題:運行時數據區

文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    如何縮短Vivado的運行時

    在Vivado Implementation階段,有時是有必要分析一下什么原因導致運行時間(runtime)過長,從而找到一些方法來縮短運行時間。
    的頭像 發表于 05-29 14:37 ?1.4w次閱讀
    如何縮短Vivado的<b class='flag-5'>運行時</b>間

    Labview 運行時內存增加

    的dll,用庫函數直接調用這個dll后,Labview運行時所占的內存基本上保持在0.9 M左右,不會卡死了。附件里是那個網友上傳的dll,大家可以下載后將.jpg改為.dll
    發表于 05-19 14:38

    Jvm的整體結構和特點

    換成java.lang.Class類的一個實例?! ?、運行時數據  元數據  JDK1.8開始的說法,之前稱為方法Method-Ar
    發表于 01-05 17:23

    C語言內存運行時不同變量是怎樣分配的

    C語言內存運行時不同變量是怎樣分配的?怎樣驗證C語言編譯后的內存地址分配是否合理?
    發表于 02-25 06:37

    請問單片機運行時內存是如何分配的?

    請問單片機運行時內存是如何分配的? 是在鏈接腳本中人工定義?還是編譯器根據某種算法自動分配?
    發表于 09-27 08:16

    java線程內存模型

    一、Java內存模型 按照官方的說法:Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 JVM主要管理兩種類型
    發表于 09-27 10:55 ?0次下載
    java線程<b class='flag-5'>內存</b>模型

    Java內存模型及原理分析

    一、Java內存模型 按照官方的說法:Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 JVM主要管理兩種類型
    發表于 09-28 11:49 ?0次下載
    Java<b class='flag-5'>內存</b>模型及原理分析

    探討JVM內存布局

    JVM內存布局規定了Java在運行過程中內存申請、分配、管理的策略,保證了JVM的穩定高效運行。
    的頭像 發表于 09-09 15:57 ?595次閱讀

    Go運行時:4年之后

    自 2018 年以來,Go GC,以及更廣泛的 Go 運行時,一直在穩步改進。近日,Go 社區總結了 4 年來 Go 運行時的一些重要變化。
    的頭像 發表于 11-30 16:21 ?551次閱讀

    jvm內存溢出故障排查

    溢出故障排查的方法和步驟。 確認內存溢出錯誤 首先,我們需要確認應用程序是否確實發生了內存溢出錯誤。內存溢出通常會被JVM報告為OutOfMemoryError。這是一個致命錯誤,暗示
    的頭像 發表于 12-05 11:04 ?389次閱讀

    jvm內存模型和內存結構

    內存模型是指Java程序在運行時,JVM內存空間的組織和管理方式。它包括了線程私有的部分和線程共享的部分。 線程私有部分 線程私有部分主要包含了棧(Stack)和程序計數器(Prog
    的頭像 發表于 12-05 11:08 ?437次閱讀

    jvm運行時內存區域劃分

    JVM是Java Virtual Machine(Java虛擬機)的縮寫,它是Java編程語言的運行環境。JVM的主要功能是將Java源代碼轉換為機器代碼,并且在運行時管理Java程序
    的頭像 發表于 12-05 14:08 ?274次閱讀

    jvm管理的內存包括哪幾個運行時數據內存

    JVM(Java虛擬機)是Java程序的運行環境,它提供了內存管理機制來管理Java程序所需的運行時數據內存。這些
    的頭像 發表于 12-05 14:09 ?241次閱讀

    jvm內存區域中,哪一塊是屬于線程共享

    是如何劃分的。JVM內存區域主要分為以下幾個部分:程序計數器、Java虛擬機棧、本地方法棧、堆、方法區和運行時常量池。其中,程序計數器、Java虛擬機棧、本地方法棧是線程私有的,而堆、方法區和
    的頭像 發表于 12-05 14:14 ?629次閱讀

    eclipse設置jvm內存大小

    內存大小,并對其背后的原理進行解釋。 JVM(Java虛擬機)是Java程序的運行環境,它負責將Java字節碼翻譯成機器碼,以便在不同的平臺上執行。JVM使用
    的頭像 發表于 12-06 11:43 ?814次閱讀
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>