JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析)
1.是什么
(1) 基本概念:可運(yùn)行 Java 代碼的非真實(shí)計(jì)算機(jī) ,包括一套字節(jié)碼指令集、一組寄存器、一個(gè)棧、一個(gè)垃圾回器,堆和一個(gè)存儲(chǔ)方法域。它運(yùn)行在操作系統(tǒng)之上,與硬件沒有直接的交互。
(2) 運(yùn)行過程:Java 源文件.java通過編譯器javac,能夠生產(chǎn)相應(yīng)的.class字節(jié)碼文件,而字節(jié)碼文件又通過 Java 虛擬機(jī)中的解釋器,編譯成特定機(jī)器上的機(jī)器碼。
不同平臺(tái)的解釋器不同,但是編譯的過程是相同的,這就是所謂的一次編譯到處執(zhí)行跨平臺(tái) 。一個(gè)程序開始運(yùn)行java -jar xxx.jar虛擬機(jī)就會(huì)進(jìn)行實(shí)例化,多個(gè)程序啟動(dòng)就會(huì)存在多個(gè)虛擬機(jī)實(shí)例。程序退出或者關(guān)閉,則虛擬機(jī)實(shí)例被銷毀,多個(gè)虛擬機(jī)實(shí)例之間數(shù)據(jù)不能共享。
2.內(nèi)存結(jié)構(gòu)
內(nèi)存是非常重要的系統(tǒng)資源,是硬盤和 CPU 的中間倉庫及橋梁,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行。JVM 內(nèi)存布局規(guī)定了 Java 程序在運(yùn)行過程中的內(nèi)存申請(qǐng)、分配、管理策略,保證了它的高效穩(wěn)定運(yùn)行。不同的 JVM 對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異。
JVM 運(yùn)行時(shí)管理的內(nèi)存就是虛擬機(jī)內(nèi)存 它會(huì)把這些內(nèi)存分配成不同的區(qū)域, JVM 利用到的沒有直接管理的物理內(nèi)存就是本地內(nèi)存,這兩種內(nèi)存有一定的區(qū)別:
-
虛擬機(jī)內(nèi)存:受虛擬機(jī)內(nèi)存參數(shù)控制 -Xmx 設(shè)置; Runtime.getRuntime().maxMemory() 進(jìn)行查看,超過參數(shù)設(shè)置的大小時(shí)就會(huì)報(bào)OOM。
-
本地內(nèi)存:本地內(nèi)存不受虛擬機(jī)內(nèi)存參數(shù)的限制,只受物理內(nèi)存容量的限制,雖然不受參數(shù)的限制,但是如果內(nèi)存的占用超出物理內(nèi)存的大小,同樣也會(huì)報(bào)OOM。
JVM 定義了若干種程序運(yùn)行期間會(huì)使用到的運(yùn)行時(shí)數(shù)據(jù)區(qū)內(nèi)存區(qū)域,其中有一些會(huì)隨著虛擬機(jī)啟動(dòng)而創(chuàng)建,隨著虛擬機(jī)退出或關(guān)閉而銷毀。另外一些則是與線程一一對(duì)應(yīng)的,這些與線程一一對(duì)應(yīng)的數(shù)據(jù)區(qū)域會(huì)隨著線程開始和結(jié)束而創(chuàng)建和銷毀。
- 線程共享:方法區(qū)、堆、堆外內(nèi)存(永久代或元空間、代碼緩存)
- 線程私有:棧、程序計(jì)數(shù)器、本地方法棧
下圖是 JVM 整體架構(gòu),中間部分就是它定義的各種運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
3.堆內(nèi)存空間
Heap Area 堆是 JVM 內(nèi)存中最大的一塊,被垃圾收集器管理也被所有線程共享。主要存放對(duì)象實(shí)例,由于 JVM 的發(fā)展,堆中也多了許多東西:
堆可以是處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可,像磁盤空間一樣,既可以是固定大小,也可以是可擴(kuò)展的(通過參數(shù)-Xmx和-Xms設(shè)定),堆在無法擴(kuò)展或者無法分配內(nèi)存時(shí)會(huì)報(bào) OOM 也就是堆內(nèi)存耗盡,堆內(nèi)存邏輯上被劃分成 3?? 個(gè)區(qū)域(分代的唯一理由就是優(yōu)化 GC 性能):
- 新生區(qū)(年輕代):新對(duì)象和沒達(dá)到一定年齡的對(duì)象都在新生區(qū)【占堆的1/3】。
- 養(yǎng)老區(qū)(老年代):被長時(shí)間使用的 MinorGC 未回收的對(duì)象【占堆的2/3】。
- 元空間(JDK8之前叫永久區(qū)):像一些方法中操作的臨時(shí)對(duì)象等,JDK8之前是占用JVM內(nèi)存,JDK8使用本地內(nèi)存這也是為什么在限制了JVM堆內(nèi)存之后程序處理大量數(shù)據(jù)時(shí)電腦內(nèi)存還是被大量占用的原因。
3.1 新生區(qū) (New Space/Young Generation)
新生區(qū)是所有新對(duì)象存儲(chǔ)的地方,被分為 3?? 個(gè)區(qū)域:伊甸區(qū)(Eden Space)和兩個(gè)幸存區(qū)(Survivor Space,被稱為 from survivor/to survivor 或 s0/s1),默認(rèn)比例是8:1:1。
剛被new出來的對(duì)象都是存放在伊甸區(qū)【如果新創(chuàng)建的對(duì)象占用內(nèi)存很大,超過了-XX:PetenureSizeThreshold 則直接分配到養(yǎng)老區(qū)】,當(dāng)前空間用完時(shí),程序又需要?jiǎng)?chuàng)建對(duì)象,JVM 的垃圾回收器將對(duì)伊甸區(qū)進(jìn)行垃圾回收(這種垃圾收集稱為MinorGC),銷毀伊甸區(qū)中不再被其他對(duì)象所引用的對(duì)象并將剩余對(duì)象移動(dòng)到s0區(qū)。若s0區(qū)也滿了,再對(duì)該區(qū)進(jìn)行垃圾回收,然后移動(dòng)到s1區(qū)。那如果s1區(qū)也滿了呢?再移動(dòng)到養(yǎng)老區(qū)。若養(yǎng)老區(qū)也滿了,那么這個(gè)時(shí)候?qū)a(chǎn)生 MajorGC(FullGC),進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理。若養(yǎng)老區(qū)執(zhí)行了 FullGC 之后發(fā)現(xiàn)依然無法進(jìn)行對(duì)象的保存,就會(huì)產(chǎn)生OOM。
MinorGC的過程為(復(fù)制->清空->互換):
【問題:為什么要將新生區(qū)分為三個(gè)區(qū)域?】由于年輕代的垃圾回收算法,(復(fù)制算法)設(shè)置兩個(gè)Survivor區(qū)最大的好處就是解決了碎片化,剛剛新建的對(duì)象在Eden中,經(jīng)歷一次Minor GC,Eden中的存活對(duì)象就會(huì)被移動(dòng)到S0,Eden被清空;等Eden區(qū)再滿了,就再觸發(fā)一次Minor GC,Eden和S0中的存活對(duì)象又會(huì)被復(fù)制送入S1區(qū)(這個(gè)過程非常重要,因?yàn)檫@種復(fù)制算法保證了S1中來自S0和Eden兩部分的存活對(duì)象占用連續(xù)的內(nèi)存空間,避免了碎片化的發(fā)生),接著新對(duì)象繼續(xù)分配在Eden區(qū)和另外那塊開始被使用的Survivor區(qū),然后始終保持一塊Survivor區(qū)是空著的,就這樣一直循環(huán)使用這三塊內(nèi)存區(qū)域。
JVM 會(huì)給new出來的對(duì)象定義一個(gè)對(duì)象年輕計(jì)數(shù)器每次的 MinorGC 對(duì)象的年齡就會(huì)+1達(dá)到老年的標(biāo)準(zhǔn)-XX:MaxTenuringThreshold【默認(rèn)值為15】則復(fù)制到養(yǎng)老區(qū):
3.2 養(yǎng)老區(qū)(Tenure Space/Old Generation)
養(yǎng)老區(qū)主要存放應(yīng)用程序中生命周期長的內(nèi)存對(duì)象。老年代的對(duì)象比較穩(wěn)定,所以Major GC不會(huì)頻繁執(zhí)行。在進(jìn)行Major GC前一般都先進(jìn)行了一次Minor GC,使得有新生代的對(duì)象晉升入老年代,導(dǎo)致空間不夠用時(shí)才觸發(fā)。
大對(duì)象直接進(jìn)入養(yǎng)老區(qū)(大對(duì)象是指需要大量連續(xù)內(nèi)存空間的對(duì)象,超過了-XX:PetenureSizeThreshold)。這樣做的目的是避免在 Eden 區(qū)和兩個(gè) Survivor 區(qū)之間發(fā)生大量的內(nèi)存拷貝。當(dāng)無法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對(duì)象時(shí)也會(huì)提前觸發(fā)一次Major GC進(jìn)行垃圾回收騰出空間。
Major GC采用標(biāo)記清除算法:首先掃描一次所有老年代,標(biāo)記出存活的對(duì)象,然后回收沒有標(biāo)記的對(duì)象。因?yàn)橐獟呙柙倩厥账訫ajor GC的耗時(shí)比較長。Major GC會(huì)產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,一般需要進(jìn)行合并或者標(biāo)記出來方便下次直接分配。當(dāng)養(yǎng)老區(qū)也滿了裝不下的時(shí)候,就會(huì)拋出OOM。
3.3 元空間(Meta Space/Permanent Generation)
不管是 JDK8 之前的永久代,還是 JDK8 及以后的元空間,都可以看作是 JVM 規(guī)范中方法區(qū)的實(shí)現(xiàn)。雖然 JVM 規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫 Non-Heap(非堆),目的應(yīng)該是與 Java 堆區(qū)分開。
永久存儲(chǔ)區(qū)是一個(gè)常駐內(nèi)存區(qū)域,用于存放 JDK 自身所攜帶的 Class、Interface 的元數(shù)據(jù),也就是說它存儲(chǔ)的是運(yùn)行環(huán)境必須的類信息,被裝載進(jìn)此區(qū)域的數(shù)據(jù)是不會(huì)被垃圾回收器回收掉的,關(guān)閉 JVM 才會(huì)釋放此區(qū)域所占用的內(nèi)存。
如果出現(xiàn)java.lang.OutOfMemoryError: PermGen space,說明是 JVM 對(duì)永久代 Perm 內(nèi)存設(shè)置不夠。一般出現(xiàn)這種情況,都是程序啟動(dòng)需要加載大量的第三方 jar 包。例如:在一個(gè)Tomcat下部署了太多的應(yīng)用。或者大量動(dòng)態(tài)反射生成的類不斷被加載,最終導(dǎo)致 Perm 區(qū)被占滿。
| JDK6及之前 | 有永久代 | 在方法區(qū) |
| JDK7 | 有永久代,已逐步“去永久代” | 在堆 |
| JDK8及之后 | 無永久代 | 在元空間 |
4.堆內(nèi)存參數(shù)
Java 堆用于存儲(chǔ) Java 對(duì)象實(shí)例,那么堆的大小在 JVM 啟動(dòng)的時(shí)候就確定了,我們可以通過 -Xmx 和 -Xms 來設(shè)定
- -Xmx 堆的起始內(nèi)存,默認(rèn)情況下為服務(wù)器內(nèi)存的1/64,等價(jià)于 -XX:InitialHeapSize
- -Xms 堆的最大內(nèi)存,默認(rèn)情況下為服務(wù)器內(nèi)存的1/4,等價(jià)于 -XX:MaxHeapSize
如果堆的內(nèi)存大小超過 -Xms 設(shè)定的最大內(nèi)存, 就會(huì)拋出OOM。通常會(huì)將 -Xmx 和 -Xms 兩個(gè)參數(shù)配置為相同的值,其目的是為了能夠在垃圾回收機(jī)制清理完堆區(qū)后不再需要重新分隔計(jì)算堆的大小,從而提高性能。
可以通過代碼獲取到我們的設(shè)置值,當(dāng)然也可以模擬 OOM:
public static void main(String[] args) {// JVM 堆大小long totalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;// JVM 堆的最大內(nèi)存long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + totalMemory + "M");System.out.println("-Xmx : " + maxMemory + "M");// 反向計(jì)算計(jì)算間內(nèi)存System.out.println("系統(tǒng)內(nèi)存大小:" + totalMemory * 64 / 1024 + "G");System.out.println("系統(tǒng)內(nèi)存大小:" + maxMemory * 4 / 1024 + "G");}5.堆內(nèi)存分配
在默認(rèn)不配置 JVM 堆內(nèi)存大小的情況下,JVM 根據(jù)默認(rèn)值來配置當(dāng)前內(nèi)存大小:
- 默認(rèn)情況下新生區(qū)和養(yǎng)老區(qū)的比例是1:2,可以通過 –XX:NewRatio 來配置。
- 新生代中的 Eden:From Survivor:To Survivor 的比例是8:1:1,可以通過-XX:SurvivorRatio來配置。
- 若在JDK7中開啟了 -XX:+UseAdaptiveSizePolicy,JVM 會(huì)動(dòng)態(tài)調(diào)整 JVM 堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡,此時(shí) –XX:NewRatio 和 -XX:SurvivorRatio 將會(huì)失效;而 JDK8 是默認(rèn)開啟-XX:+UseAdaptiveSizePolicy在 JDK8中,不要隨意關(guān)閉-XX:+UseAdaptiveSizePolicy,除非對(duì)堆內(nèi)存的劃分有明確的規(guī)劃。
每次 GC 后都會(huì)重新計(jì)算 Eden、From Survivor、To Survivor 的大小計(jì)算依據(jù)是 GC 過程中統(tǒng)計(jì)的GC時(shí)間、吞吐量、內(nèi)存占用量:
# 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)# 查看進(jìn)程的堆信息 jmap -heap 進(jìn)程號(hào)6.堆內(nèi)存回收(垃圾回收)
JVM 在進(jìn)行 GC 時(shí),并非每次都對(duì)堆內(nèi)存(新生區(qū)、養(yǎng)老區(qū)、方法區(qū))區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。針對(duì) HotSpot VM 的實(shí)現(xiàn),它里面的 GC 按照回收區(qū)域又分為兩大類:
(1)整堆收集(FullGC):收集整個(gè) Java 堆和方法區(qū)的垃圾。
(2)部分收集:不是完整收集整個(gè) Java 堆的垃圾收集。其中又分為:
新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集。
老年代收集(MajorGC/OldGC):只是老年代的垃圾收集。
目前只有 G1 GC 會(huì)有這種行為
目前,只有 CMS GC 會(huì)有單獨(dú)收集老年代的行為
很多時(shí)候 MajorGC 會(huì)和 FullGC 混合使用,需要具體分辨是老年代回收還是整堆回收
混合收集(Mixed GC):收集整個(gè)新生代以及部分老年代的垃圾收集
7.TLAB(Thread Local Allocation Buffer)
從內(nèi)存模型而不是垃圾回收的角度,對(duì) Eden 區(qū)域繼續(xù)進(jìn)行劃分,JVM 為每個(gè)線程分配了一個(gè)私有緩存區(qū)域,它包含在 Eden 空間內(nèi)多線程同時(shí)分配內(nèi)存時(shí),使用 TLAB 可以避免一系列的非線程安全問題,同時(shí)還能提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方式稱為快速分配策略。為什么要有 TLAB:
- 堆區(qū)是線程共享的,任何線程都可以訪問到堆區(qū)中的共享數(shù)據(jù)
- 由于對(duì)象實(shí)例的創(chuàng)建在 JVM 中非常頻繁,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的
- 為避免多個(gè)線程操作同一地址,需要使用加鎖等機(jī)制,進(jìn)而影響分配速度
盡管不是所有的對(duì)象實(shí)例都能夠在 TLAB 中成功分配內(nèi)存,但 JVM 確實(shí)是將 TLAB 作為內(nèi)存分配的首選。在程序中,可以通過 -XX:UseTLAB 設(shè)置是否開啟 TLAB 空間。默認(rèn)情況下,TLAB 空間的內(nèi)存非常小,僅占有整個(gè) Eden 空間的 1%,我們可以通過 -XX:TLABWasteTargetPercent 設(shè)置 TLAB 空間所占用 Eden 空間的百分比大小。一旦對(duì)象在 TLAB 空間分配內(nèi)存失敗時(shí),JVM 就會(huì)嘗試著通過使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,從而直接在 Eden 空間中分配內(nèi)存。
8.堆是分配對(duì)象存儲(chǔ)的唯一選擇嗎
隨著 JIT 編譯期的發(fā)展和逃逸分析技術(shù)的逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,所有的對(duì)象都分配到堆上也漸漸變得不那么“絕對(duì)”了。 ——《深入理解 Java 虛擬機(jī)》
逃逸分析(Escape Analysis)是目前 Java 虛擬機(jī)中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少 Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過逃逸分析,Java Hotspot 編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:
- 當(dāng)一個(gè)對(duì)象在方法中被定義后,對(duì)象只在方法內(nèi)部使用,則認(rèn)為沒有發(fā)生逃逸。
- 當(dāng)一個(gè)對(duì)象在方法中被定義后,它被外部方法所引用,則認(rèn)為發(fā)生逃逸。例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
例如:
public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }StringBuffer sb是一個(gè)方法內(nèi)部變量,上述代碼中直接將sb返回,這樣這個(gè) StringBuffer 有可能被其他方法所改變,這樣它的作用域就不只是在方法內(nèi)部,雖然它是一個(gè)局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實(shí)例變量,稱為線程逃逸。
上述代碼如果想要 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 將不會(huì)逃逸出方法。
參數(shù)設(shè)置:
- 在 JDK 6u23版本之后,HotSpot 中默認(rèn)就已經(jīng)開啟了逃逸分析
- 如果使用較早版本,可以通過-XX"+DoEscapeAnalysis顯式開啟
開發(fā)中使用局部變量,就不要在方法外定義。使用逃逸分析,編譯器可以對(duì)代碼做優(yōu)化:
- 棧上分配:將堆分配轉(zhuǎn)化為棧分配。如果一個(gè)對(duì)象在子程序中被分配,要使指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象可能是棧分配的候選,而不是堆分配
- 同步省略:如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步
- 分離對(duì)象或標(biāo)量替換:有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而存儲(chǔ)在 CPU 寄存器
JIT 編譯器在編譯期間根據(jù)逃逸分析的結(jié)果,發(fā)現(xiàn)如果一個(gè)對(duì)象并沒有逃逸出方法的話,就可能被優(yōu)化成棧上分配。分配完成后,繼續(xù)在調(diào)用棧內(nèi)執(zhí)行,最后線程結(jié)束,棧空間被回收,局部變量對(duì)象也被回收。這樣就無需進(jìn)行垃圾回收了。
常見棧上分配的場景:成員變量賦值、方法返回值、實(shí)例引用傳遞
8.1 代碼優(yōu)化之同步省略(消除)
- 線程同步的代價(jià)是相當(dāng)高的,同步的后果是降低并發(fā)性和性能
- 在動(dòng)態(tài)編譯同步塊的時(shí)候,JIT 編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對(duì)象是否能夠被一個(gè)線程訪問而沒有被發(fā)布到其他線程。如果沒有,那么 JIT 編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這個(gè)代碼的同步。這樣就能大大提高并發(fā)性和性能。這個(gè)取消同步的過程就叫做同步省略,也叫鎖消除。
如上代碼,代碼中對(duì) keeper 這個(gè)對(duì)象進(jìn)行加鎖,但是 keeper 對(duì)象的生命周期只在 keep()方法中,并不會(huì)被其他線程所訪問到,所以在 JIT編譯階段就會(huì)被優(yōu)化掉。優(yōu)化成:
public void keep() {Object keeper = new Object();System.out.println(keeper); }8.2 代碼優(yōu)化之標(biāo)量替換
標(biāo)量(Scalar)是指一個(gè)無法再分解成更小的數(shù)據(jù)的數(shù)據(jù)。Java 中的原始數(shù)據(jù)類型就是標(biāo)量。
相對(duì)的,那些的還可以分解的數(shù)據(jù)叫做聚合量(Aggregate),Java 中的對(duì)象就是聚合量,因?yàn)槠溥€可以分解成其他聚合量和標(biāo)量。
在 JIT 階段,通過逃逸分析確定該對(duì)象不會(huì)被外部訪問,并且對(duì)象可以被進(jìn)一步分解時(shí),JVM不會(huì)創(chuàng)建該對(duì)象,而會(huì)將該對(duì)象成員變量分解若干個(gè)被這個(gè)方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。這個(gè)過程就是標(biāo)量替換。
通過 -XX:+EliminateAllocations 可以開啟標(biāo)量替換,-XX:+PrintEliminateAllocations 查看標(biāo)量替換情況。
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 對(duì)象并沒有逃逸出alloc()方法,并且 point 對(duì)象是可以拆解成標(biāo)量的。那么,JIT 就不會(huì)直接創(chuàng)建 Point 對(duì)象,而是直接使用兩個(gè)標(biāo)量 int x ,int y 來替代 Point 對(duì)象。
private static void alloc() {int x = 1;int y = 2;System.out.println("point.x="+x+"; point.y="+y); }8.3 代碼優(yōu)化之棧上分配
通過 JVM 內(nèi)存分配可以知道 JAVA 中的對(duì)象都是在堆上進(jìn)行分配,當(dāng)對(duì)象沒有被引用的時(shí)候,需要依靠 GC 進(jìn)行回收內(nèi)存,如果對(duì)象數(shù)量較多的時(shí)候,會(huì)給 GC 帶來較大壓力,也間接影響了應(yīng)用的性能。為了減少臨時(shí)對(duì)象在堆內(nèi)分配的數(shù)量,JVM 通過逃逸分析確定該對(duì)象不會(huì)被外部訪問。那就通過標(biāo)量替換將該對(duì)象分解在棧上分配內(nèi)存,這樣該對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
**其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經(jīng)過逃逸分析可以做標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過程。**一個(gè)極端的例子,就是經(jīng)過逃逸分析之后,發(fā)現(xiàn)沒有一個(gè)對(duì)象是不逃逸的。那這個(gè)逃逸分析的過程就白白浪費(fèi)掉了。
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的JVM【带着问题去学习 01】什么是JVM+内存结构+堆内存+堆内存参数(逃逸分析)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Java报错】GP数据库 functi
- 下一篇: JVM【带着问题去学习 02】数据结构栈