日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析)

發布時間:2024/10/6 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1.是什么

(1) 基本概念:可運行 Java 代碼的非真實計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回器,堆和一個存儲方法域。它運行在操作系統之上,與硬件沒有直接的交互。

(2) 運行過程:Java 源文件.java通過編譯器javac,能夠生產相應的.class字節碼文件,而字節碼文件又通過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼。

不同平臺的解釋器不同,但是編譯的過程是相同的,這就是所謂的一次編譯到處執行跨平臺 。一個程序開始運行java -jar xxx.jar虛擬機就會進行實例化,多個程序啟動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例被銷毀,多個虛擬機實例之間數據不能共享。

2.內存結構

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

JVM 運行時管理的內存就是虛擬機內存 它會把這些內存分配成不同的區域, JVM 利用到的沒有直接管理的物理內存就是本地內存,這兩種內存有一定的區別:

  • 虛擬機內存:受虛擬機內存參數控制 -Xmx 設置; Runtime.getRuntime().maxMemory() 進行查看,超過參數設置的大小時就會報OOM。

  • 本地內存:本地內存不受虛擬機內存參數的限制,只受物理內存容量的限制,雖然不受參數的限制,但是如果內存的占用超出物理內存的大小,同樣也會報OOM。

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

  • 線程共享:方法區、堆、堆外內存(永久代或元空間、代碼緩存)
  • 線程私有:棧、程序計數器、本地方法棧

下圖是 JVM 整體架構,中間部分就是它定義的各種運行時數據區域:

3.堆內存空間

Heap Area 堆是 JVM 內存中最大的一塊,被垃圾收集器管理也被所有線程共享。主要存放對象實例,由于 JVM 的發展,堆中也多了許多東西:


堆可以是處于物理上不連續的內存空間中,只要邏輯上是連續的即可,像磁盤空間一樣,既可以是固定大小,也可以是可擴展的(通過參數-Xmx和-Xms設定),堆在無法擴展或者無法分配內存時會報 OOM 也就是堆內存耗盡,堆內存邏輯上被劃分成 3?? 個區域(分代的唯一理由就是優化 GC 性能):

  • 新生區(年輕代):新對象和沒達到一定年齡的對象都在新生區【占堆的1/3】。
  • 養老區(老年代):被長時間使用的 MinorGC 未回收的對象【占堆的2/3】。
  • 元空間(JDK8之前叫永久區):像一些方法中操作的臨時對象等,JDK8之前是占用JVM內存,JDK8使用本地內存這也是為什么在限制了JVM堆內存之后程序處理大量數據時電腦內存還是被大量占用的原因。

3.1 新生區 (New Space/Young Generation)

新生區是所有新對象存儲的地方,被分為 3?? 個區域:伊甸區(Eden Space)和兩個幸存區(Survivor Space,被稱為 from survivor/to survivor 或 s0/s1),默認比例是8:1:1。

剛被new出來的對象都是存放在伊甸區【如果新創建的對象占用內存很大,超過了-XX:PetenureSizeThreshold 則直接分配到養老區】,當前空間用完時,程序又需要創建對象,JVM 的垃圾回收器將對伊甸區進行垃圾回收(這種垃圾收集稱為MinorGC),銷毀伊甸區中不再被其他對象所引用的對象并將剩余對象移動到s0區。若s0區也滿了,再對該區進行垃圾回收,然后移動到s1區。那如果s1區也滿了呢?再移動到養老區。若養老區也滿了,那么這個時候將產生 MajorGC(FullGC),進行養老區的內存清理。若養老區執行了 FullGC 之后發現依然無法進行對象的保存,就會產生OOM。

MinorGC的過程為(復制->清空->互換):

問題:為什么要將新生區分為三個區域?】由于年輕代的垃圾回收算法,(復制算法)設置兩個Survivor區最大的好處就是解決了碎片化,剛剛新建的對象在Eden中,經歷一次Minor GC,Eden中的存活對象就會被移動到S0,Eden被清空;等Eden區再滿了,就再觸發一次Minor GC,Eden和S0中的存活對象又會被復制送入S1區(這個過程非常重要,因為這種復制算法保證了S1中來自S0和Eden兩部分的存活對象占用連續的內存空間,避免了碎片化的發生),接著新對象繼續分配在Eden區和另外那塊開始被使用的Survivor區,然后始終保持一塊Survivor區是空著的,就這樣一直循環使用這三塊內存區域。

JVM 會給new出來的對象定義一個對象年輕計數器每次的 MinorGC 對象的年齡就會+1達到老年的標準-XX:MaxTenuringThreshold【默認值為15】則復制到養老區:

3.2 養老區(Tenure Space/Old Generation)

養老區主要存放應用程序中生命周期長的內存對象。老年代的對象比較穩定,所以Major GC不會頻繁執行。在進行Major GC前一般都先進行了一次Minor GC,使得有新生代的對象晉升入老年代,導致空間不夠用時才觸發。

大對象直接進入養老區(大對象是指需要大量連續內存空間的對象,超過了-XX:PetenureSizeThreshold)。這樣做的目的是避免在 Eden 區和兩個 Survivor 區之間發生大量的內存拷貝。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次Major GC進行垃圾回收騰出空間。


Major GC采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。因為要掃描再回收所以Major GC的耗時比較長。Major GC會產生內存碎片,為了減少內存損耗,一般需要進行合并或者標記出來方便下次直接分配。當養老區也滿了裝不下的時候,就會拋出OOM。

3.3 元空間(Meta Space/Permanent Generation)

不管是 JDK8 之前的永久代,還是 JDK8 及以后的元空間,都可以看作是 JVM 規范中方法區的實現。雖然 JVM 規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開。

永久存儲區是一個常駐內存區域,用于存放 JDK 自身所攜帶的 Class、Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所占用的內存。

如果出現java.lang.OutOfMemoryError: PermGen space,說明是 JVM 對永久代 Perm 內存設置不夠。一般出現這種情況,都是程序啟動需要加載大量的第三方 jar 包。例如:在一個Tomcat下部署了太多的應用。或者大量動態反射生成的類不斷被加載,最終導致 Perm 區被占滿。

版本永久代常量池
JDK6及之前有永久代在方法區
JDK7有永久代,已逐步“去永久代”在堆
JDK8及之后無永久代在元空間

4.堆內存參數

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

  • -Xmx 堆的起始內存,默認情況下為服務器內存的1/64,等價于 -XX:InitialHeapSize
  • -Xms 堆的最大內存,默認情況下為服務器內存的1/4,等價于 -XX:MaxHeapSize

如果堆的內存大小超過 -Xms 設定的最大內存, 就會拋出OOM。通常會將 -Xmx 和 -Xms 兩個參數配置為相同的值,其目的是為了能夠在垃圾回收機制清理完堆區后不再需要重新分隔計算堆的大小,從而提高性能。

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

public static void main(String[] args) {// JVM 堆大小long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;// JVM 堆的最大內存long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + totalMemory + "M");System.out.println("-Xmx : " + maxMemory + "M");// 反向計算計算間內存System.out.println("系統內存大小:" + totalMemory * 64 / 1024 + "G");System.out.println("系統內存大小:" + maxMemory * 4 / 1024 + "G");}

5.堆內存分配

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

  • 默認情況下新生區和養老區的比例是1:2,可以通過 –XX:NewRatio 來配置。
  • 新生代中的 Eden:From Survivor:To Survivor 的比例是8:1:1,可以通過-XX:SurvivorRatio來配置。
  • 若在JDK7中開啟了 -XX:+UseAdaptiveSizePolicy,JVM 會動態調整 JVM 堆中各個區域的大小以及進入老年代的年齡,此時 –XX:NewRatio 和 -XX:SurvivorRatio 將會失效;而 JDK8 是默認開啟-XX:+UseAdaptiveSizePolicy在 JDK8中,不要隨意關閉-XX:+UseAdaptiveSizePolicy,除非對堆內存的劃分有明確的規劃。

每次 GC 后都會重新計算 Eden、From Survivor、To Survivor 的大小計算依據是 GC 過程中統計的GC時間、吞吐量、內存占用量:

# JDK8 java -XX:+PrintFlagsFinal -version | grep HeapSizeuintx ErgoHeapSizeLimit = 0 {product}uintx HeapSizePerGCThread = 87241520 {product}uintx InitialHeapSize := 29360128 {product}uintx LargePageHeapSizeThreshold = 134217728 {product}uintx MaxHeapSize := 459276288 {product} java version "1.8.0_251" Java(TM) SE Runtime Environment (build 1.8.0_251-b08) Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)# 查看進程的堆信息 jmap -heap 進程號

6.堆內存回收(垃圾回收)

JVM 在進行 GC 時,并非每次都對堆內存(新生區、養老區、方法區)區域一起回收的,大部分時候回收的都是指新生代。針對 HotSpot VM 的實現,它里面的 GC 按照回收區域又分為兩大類:

(1)整堆收集(FullGC):收集整個 Java 堆和方法區的垃圾。
(2)部分收集:不是完整收集整個 Java 堆的垃圾收集。其中又分為:

新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。
老年代收集(MajorGC/OldGC):只是老年代的垃圾收集。
目前只有 G1 GC 會有這種行為
目前,只有 CMS GC 會有單獨收集老年代的行為
很多時候 MajorGC 會和 FullGC 混合使用,需要具體分辨是老年代回收還是整堆回收
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集

7.TLAB(Thread Local Allocation Buffer)

從內存模型而不是垃圾回收的角度,對 Eden 區域繼續進行劃分,JVM 為每個線程分配了一個私有緩存區域,它包含在 Eden 空間內多線程同時分配內存時,使用 TLAB 可以避免一系列的非線程安全問題,同時還能提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱為快速分配策略。為什么要有 TLAB:

  • 堆區是線程共享的,任何線程都可以訪問到堆區中的共享數據
  • 由于對象實例的創建在 JVM 中非常頻繁,因此在并發環境下從堆區中劃分內存空間是線程不安全的
  • 為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度

盡管不是所有的對象實例都能夠在 TLAB 中成功分配內存,但 JVM 確實是將 TLAB 作為內存分配的首選。在程序中,可以通過 -XX:UseTLAB 設置是否開啟 TLAB 空間。默認情況下,TLAB 空間的內存非常小,僅占有整個 Eden 空間的 1%,我們可以通過 -XX:TLABWasteTargetPercent 設置 TLAB 空間所占用 Eden 空間的百分比大小。一旦對象在 TLAB 空間分配內存失敗時,JVM 就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在 Eden 空間中分配內存。

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

隨著 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被回收,局部變量對象也被回收。這樣就無需進行垃圾回收了。

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

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

  • 線程同步的代價是相當高的,同步的后果是降低并發性和性能
  • 在動態編譯同步塊的時候,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); }

8.2 代碼優化之標量替換

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

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

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

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

public static void main(String[] args) {alloc(); }private static void alloc() {Point point = new Point1,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); }

8.3 代碼優化之棧上分配

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

**其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程。**一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。

與50位技術專家面對面20年技術見證,附贈技術全景圖

總結

以上是生活随笔為你收集整理的JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。