一、堆(Heap)的核心概述
一個程序就對應一個JVM執行個體,一個JVM執行個體就隻有一個運作時資料區,隻對應一個堆及一個方法區。
一個JVM執行個體隻存在一個堆記憶體,堆也是Java記憶體管理的核心區域。Java 堆區在JVM啟動的時候即被建立,其空間大小也就确定了。是JVM管理的最大一塊記憶體空間。
《Java虛拟機規範》規定,堆可以處于實體上不連續的記憶體空間中,但在邏輯上它應該被視為連續的。
所有的線程共享Java堆,在這裡還可以劃分線程私有的緩沖區(
ThreadLocal Allocation Buffer
,
TLAB
)。
《Java虛拟機規範》中對Java堆的描述是:所有的對象執行個體以及數組都應當在運作時配置設定在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
實際上是:“幾乎”所有的對象執行個體都在這裡配置設定記憶體。
數組和對象可能永遠不會存儲在棧上,因為棧幀中儲存引用,這個引用指向對象或者數組在堆中的位置。
在方法結束後,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
堆,是GC ( Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
二、設定堆的大小
- Xms10m:堆的初始空間為10M ,等價于 -XX:InitialHeapSize
- Xmx10M:堆的最大空間為10M, 等價于 -XX:MaxHeapSize
(-X是JVM的運作參數,ms是memory start,mx是memory max)
通常會将
-Xms
和
-Xmx
兩個參數配置相同的值,其目的是為了能夠在垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,進而提高性能。
預設情況下,初始記憶體大小:
實體電腦記憶體大小 / 64
最大記憶體大小:
實體電腦記憶體大小 / 4
public class ClassLoaderTest {
public static void main(String[] args) {
try {
Class<?> aClass = Class.forName("JVM.GetClassLoader");
ClassLoader classLoader = aClass.getClassLoader();
Console.log("【1】{}", classLoader);
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Console.log("【2】{}", contextClassLoader);
ClassLoader parent = ClassLoader.getSystemClassLoader().getParent();
Console.log("【3】{}", parent);
Thread.sleep(100000000);
} catch (ClassNotFoundException | InterruptedException e) {
e.printStackTrace();
}
}
}
運作以上代碼,啟動時設定堆記憶體為 10M,通過VisualVM可以看到eden+s0+s1+old的大小正好是10M。而堆細分下來正好是這三部分組成的。
三、堆記憶體細分
-
是JDK7及之前的叫法,JDK8後叫永久代
。元空間
- 實際實體上
不在堆記憶體中,而是在方法區。隻是邏輯上認為永久代或元空間是在堆記憶體上的。實際方法區是獨立于堆的一塊記憶體空間。元空間
-
=堆空間
+年輕代
老年代
在上面的啟動參數上加上GC日志列印參數:
上面年輕代total隻有2560K,因為年輕代的S1和S2隻會有一個在使用中,另一個始終是空的。
還以這樣檢視堆記憶體情況:
四、堆記憶體 OOM
一旦堆區中的記憶體大小超過
-Xmx
所指定的最大記憶體時,将會抛出
OutOfMemoryError
異常。
以下每20毫秒在堆中建立一個大的byte數組對象,一段時間後堆記憶體就會耗光:
public class OOMDemo {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
class Picture {
private byte[] picsize;
public Picture(int length) {
this.picsize = new byte[length];
}
}
看下VisualVM的Visual GC頁:
可以看到,老年代的對象是逐漸遞增的,最終耗盡,OOM。
可以看到是byte[]數組類型的變量造成了OOM。
五、年輕代與老年代
堆分為年輕代(
YoungGen
)和老年代(
OldGen
)
其中年輕代又可以劃分為
Eden
空間、
Survivor0
空間和
Survivor1
空間 (有時也叫做
from
區、
to
區)。
5.1 年輕代( YoungGen
)和老年代( OldGen
)的在堆中的占比
YoungGen
OldGen
年輕代(
YoungGen
)和老年代(
OldGen
)的在堆中的占比是可以按下面的方式配置的:
- 預設-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3
- 修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
以上就是預設的年輕代和老年代的比例。(20+40 = 60,新生代占1/3)
一般若老年代的對象較多時考慮修這個參數比例,将老年代擴大一些。
5.2 Eden、S0、S1在年輕代中的占比
Eden:S0:S1 預設是 8:1:1
可以通過選項
-XX:SurvivorRatio
調整這個空間比例。
六、對象在堆記憶體中的配置設定過程
- 幾乎所有的Java對象都是在Eden區被new出來的(還有一些大對象直接會在老年代生成)
- 絕大部分的Java對象的銷毀都在新生代進行了(IBM公司的研究表明,新生代中80%的對象都是“朝生夕死”的)
- 可以使用選項
設定新生代最大記憶體大小(這個參數一般使用預設值就可以了)-Xmn
為新對象配置設定記憶體是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮記憶體如何配置設定、在哪裡配置設定等問題,并且由于記憶體配置設定算法與記憶體回收算法密切相關,是以還需要考慮GC執行完記憶體回收後是否會在記憶體空間中産生記憶體碎片。
6.1 對象配置設定流程
-
的對象先放new
區。此區有大小限制。Eden
- 當
的空間填滿時,程式又需要建立對象,JVM的垃圾回收器将對Eden
區進行垃圾回收(Eden
),将MinorGC
區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到Eden
元區Eden
- 然後将
中的剩餘幸存對象移動到Eden
區(Survivor 幸存者的意思)。Survivor0
- 如果
又滿了會再次觸發Eden
,會回收MinorGC
和幸存區域的垃圾對象,此時上次幸存下來的放到Eden
區的,如果沒有回收,就會放到Survivor0
區。Survivor1
- 如果再次經曆垃圾回收,此時會重新放回
區,接着再去Survivor0
區。Survivor1
- 啥時候能去老年區呢?可以設定次數。預設是15次。可以設定參數:
進行設定。-XX:MaxTenuringThreshold=<N>
- 在老年區,相對悠閑。當老年區記憶體不足時,再次觸發GC:(
),進行老年區的記憶體清理。Major GC
- 若老年區執行了
之後發現依然無法進行對象的儲存,就會産生Major GC
異常OOM
① Eden區滿的時候才會觸發YGC或者說Minor GC ② 紅色是垃圾,數字是age,每次移動但不回收的對象age+1 ③ s0和s1之間是互為from、 to 的,裡面的對象每次Minor GC隻後會由一個轉移到另一個,它兩總是一個有對象,一個是空的,那個空的在每次Minor GC後會變成to,即每次Minor GC時Eden和另一個有值的survivor裡面幸存的對象會先移到這個空的survivor,age超過門檻值就會進入老年代。 ④ age預設是15,即15次Minor GC後,再幸存的對象都會進入老年代。
注意:
- 隻有
區滿的時候才會觸發Minor GC,survivor區域滿了不會觸發Eden
- 針對幸存者s0,s1區的總結:複制之後有交換,誰空誰是to
- 關于垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區(元空間)收集
- 如果對象在
出生并經過第一次Eden
後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并将對象年齡設為1。不能被Survivor容納的,則會直接進入老年代,若老年代也放不下就會觸發Full GC,之後再放不下就OOMMinor GC
6.2 對象配置設定特殊的情況
上面是一般的對象記憶體配置設定過程,還有一些特殊的情況:
特殊情況主要是針對一些大對象的記憶體配置設定。
所謂的大對象,比如代碼中比較長的字元串或者數組,需要連續的記憶體空間
上面介紹了年輕代和老年代的占堆記憶體的占比預設是1:2 (
-XX:NewRatio=2
),而儲存新new出來的對象的Eden區域預設隻占年輕代的80%,是以年輕代的空間有限。
一些超大對象在Eden中申請記憶體可能不夠,此時就直接在老年代去建立。
若老年代也放不下,會觸發FullGC,再往老年代放,若還是放不下就會OOM。
6.3 對象提升(Promotion)規則
針對不同年齡段的對象配置設定原則如下:
- 優先配置設定到
Eden
- 大對象
放不下時直接配置設定到老年代(應盡量避免程式中出現過多的大對象)Eden
- 長期存活的對象配置設定到老年代
-
動态對象年齡判斷
➢如果
中相同年齡的所有對象大小的總和大于Survivor
空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到Survivor
中要求的年齡MaxTenuringThreshold
- 空間配置設定擔保
➢空間配置設定擔保是說,-XX:HandlePromotionFailure
之後沒有幾個對象被回收,極端的情況是一個對象都沒回收,這個時候年輕代就存不下這個新來的對象了(存的下也不會MinorGC了),那就會使用這個空間配置設定擔保,把Minor GC
區域無法容納的對象儲存到老年代(前提是老年代空間富餘)survivor
七、配置設定過程示範
以上每隔10ms就建立一個HeapInstanceTest類型的對象放入List,這個對象又包含一個大的buffer對象,上面的介紹可知,建立的對象都是先放到Eden區域,當Eden滿了就會觸發Minor GC,因為有長生命周期的list對象引用在,是以這些對象都不會被回收,經過幾番from、to之後最終會進入老年代,是以老年代對象會越來越多,最終OOM。
JvisualVM如下:
結合GC日志詳情看下:
[GC (Allocation Failure) [PSYoungGen: 153600K->25585K(179200K)] 153600K->105194K(588800K), 0.0301019 secs] [Times: user=0.09 sys=0.33, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 179185K->25590K(179200K)] 258794K->243887K(588800K), 0.0466722 secs] [Times: user=0.11 sys=0.50, real=0.05 secs]
[GC (Allocation Failure) [PSYoungGen: 179169K->25587K(179200K)] 397466K->389284K(588800K), 0.0486050 secs] [Times: user=0.08 sys=0.52, real=0.05 secs]
[Full GC (Ergonomics) [PSYoungGen: 25587K->0K(179200K)] [ParOldGen: 363697K->388293K(409600K)] 389284K->388293K(588800K), [Metaspace: 9668K->9668K(1058816K)], 0.0971607 secs] [Times: user=1.03 sys=0.00, real=0.10 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->126664K(179200K)] [ParOldGen: 388293K->409332K(409600K)] 541893K->535996K(588800K), [Metaspace: 9729K->9729K(1058816K)], 0.0465593 secs] [Times: user=0.50 sys=0.09, real=0.05 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->152490K(179200K)] [ParOldGen: 409332K->409332K(409600K)] 562932K->561822K(588800K), [Metaspace: 9734K->9734K(1058816K)], 0.0135729 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153046K(179200K)] [ParOldGen: 409332K->409544K(409600K)] 562932K->562590K(588800K), [Metaspace: 9734K->9667K(1058816K)], 0.1266872 secs] [Times: user=1.27 sys=0.00, real=0.13 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153174K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562719K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0186866 secs] [Times: user=0.31 sys=0.00, real=0.02 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153174K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562719K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0112202 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153315K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562860K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0107946 secs] [Times: user=0.20 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153425K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562970K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0098381 secs] [Times: user=0.20 sys=0.02, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153425K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562970K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0097163 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 153600K->153425K(179200K)] [ParOldGen: 409544K->409544K(409600K)] 563144K->562970K(588800K), [Metaspace: 9667K->9667K(1058816K)], 0.0097015 secs] [Times: user=0.20 sys=0.02, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 153425K->153425K(179200K)] [ParOldGen: 409544K->409507K(409600K)] 562970K->562933K(588800K), [Metaspace: 9667K->9589K(1058816K)], 0.0839591 secs] [Times: user=0.86 sys=0.05, real=0.08 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at JVM.HeapInstanceTest.<init>(HeapInstanceTest.java:7)
at JVM.HeapInstanceTest.main(HeapInstanceTest.java:12)
[Full GC (Ergonomics) [PSYoungGen: 153600K->0K(179200K)] [ParOldGen: 409517K->2599K(409600K)] 563117K->2599K(588800K), [Metaspace: 9620K->9620K(1058816K)], 0.0132342 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 179200K, used 1590K [0x00000000f3800000, 0x0000000100000000, 0x0000000100000000)
eden space 153600K, 1% used [0x00000000f3800000,0x00000000f398d8a8,0x00000000fce00000)
from space 25600K, 0% used [0x00000000fce00000,0x00000000fce00000,0x00000000fe700000)
to space 25600K, 0% used [0x00000000fe700000,0x00000000fe700000,0x0000000100000000)
ParOldGen total 409600K, used 2599K [0x00000000da800000, 0x00000000f3800000, 0x00000000f3800000)
object space 409600K, 0% used [0x00000000da800000,0x00000000daa89c58,0x00000000f3800000)
Metaspace used 9624K, capacity 9922K, committed 10240K, reserved 1058816K
class space used 1070K, capacity 1158K, committed 1280K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:62032', transport: 'socket'
可以看到報
OOM
前會進行至少一次
Full GC
。
另外,若沒有發生GC,是不會列印GC日志的。
八、Minor GC、Major GC、Full GC
JVM在進行GC時,并非每次都對上面三個記憶體(新生代、老年代、元空間)區域一起回收的,大部分時候回收的都是指新生代。
針對HotSpot VM的實作,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
-
部分收集:
➢新生代收集(Minor GC / Young GC) :隻是新生代的垃圾收集
➢老年代收集(Major GC / Old GC) :隻是老年代的垃圾收集。
目前,隻有CMS GC(并發垃圾回收器)會有單獨收集老年代的行為。
注意很多時候Major GC會和Full GC混淆使用,要具體分辨是老年代回收還是整堆回收。
-
混合收集(Mixed GC): 收集整個新生代以及部分老年代的垃圾收集。
目前,隻有G1 GC會有這種行為
- 整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。
8.1 Minor GC 觸發條件
- 當年輕代空間不足時,就會觸發
,這裡的年輕代滿指的是Minor GC
代滿,Eden
滿不會引發GC(但每次Survivor
會清理年輕代Minor GC
+Eden
的記憶體)Survivor
- Java對象大多都具備朝生夕滅的特性,Minor GC非常頻繁,一般回收速度也比較快。
- Minor GC會引發
(Stop The World), 暫停其它使用者的線程,等垃圾回收結束,使用者線程才恢複運作。STW
8.2 Major GC 觸發條件
- 指發生在老年代的GC,對象從老年代消失時,我們說
發生了Major GC
- 出現了
,經常會伴随至少一次的Major GC
(但非絕對的,在Minor GC
收集器的收集政策裡就有直接進行ParallelScavenge
的政策選擇過程)。Major GC
- 也就是在老年代空間不足時,會先嘗試觸發
。如果之後空間還不足,則觸發Minor GC
Major GC
-
的速度一般會比Major GC
慢10倍以上,Minor GC
的時間更長。STW
- 如果
後,記憶體還不足,就報Major GC
了。OOM
8.3 Full GC 觸發機制
觸發Full GC執行的情況有如下五種:
- 調用System.gc()時,系統建議執行Full GC,但是不必然執行
- 老年代空間不足
- 方法區空間不足
- 通過Minor GC後進入老年代的平均大小大于老年代的可用記憶體
- 由
區、Eden
(From Space)區向Survivor0
(To Space)區複制時,對象大小大于To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小于該對象大小Survivor1
Full GC
是開發或調優中盡量要避免的,這樣暫時時間會短一些。
九、堆空間分代思想
為什麼需要把Java堆分代?不分代就不能正常工作了嗎?
經研究,不同對象的生命周期不同,70%-99%的對象是臨時對象。
➢新生代:有Eden、 兩塊大小相同的Survivor (又稱為from/to,s0/s1) 構成,to總為空。.
➢老年代:存放新生代中經曆多次GC仍然存活的對象。
其實不分代完全可以,分代的唯一理由就是優化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新建立的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣在更短的時間能騰出更大的空間。
十、TLAB
TLAB
(
Thread Local Allocation Buffer
)線程本地配置設定緩沖區
10.1 為什麼有TLAB ?
- 堆區是線程共享區域,任何線程都可以通路到堆區中的共享資料。
- 由于對象執行個體的建立在JVM中非常頻繁,是以在并發環境下從堆區中劃分記憶體空間是線程不安全的,為避免多個線程操作同一位址,需要使用加鎖等機制,進而影響配置設定速度。
10.2 什麼是TLAB ?
- 從記憶體模型而不是垃圾收集的角度,對
區域繼續進行劃分,JVM為每個線程配置設定了一個私有緩存區域,它包含在Eden
空間内。Eden
- 多線程同時配置設定記憶體時,使用
可以避免一系列的非線程安全問題,同時還能夠提升記憶體配置設定的吞吐量TLAB
- 盡管不是所有的對象執行個體都能夠在TLAB中成功配置設定記憶體,但JVM确實是将TLAB作為記憶體配置設定的首選
- 在程式中,開發人員可以通過選項
設定是否開啟-XX:UseTLAB
空間TLAB
- 預設情況下,TLAB空間的記憶體非常小,僅占有整個Eden空間的1%,可以通過選項
設定-XX:TLABWasteTargetPercent
空間所占用TLAB
空間的百分比大小Eden
- 一旦對象在
空間配置設定記憶體失敗時,JVM就會嘗試着通過使用加鎖機制確定資料操作的原子性,進而直接在TLAB
空間中配置設定記憶體Eden
- 這一塊是堆記憶體線程私有的區域,是以堆記憶體也不完全是資料共享的
十一、堆相關參數
-
:檢視所有的參數的預設初始值-XX:+PrintFlagsInitial
-
:檢視所有的參數的最終值(可能會存在修改,不再是初始值)-XX:+PrintFlagsFinal
-
:初始堆空間記憶體( 預設為實體記憶體的1/64)-Xms
-
:最大堆空間記憶體(預設為實體記憶體的1/4)-Xmx
-
:設定新生代的大小。(初始值 及最大值)-Xmn
-
:配置新生代與老年代在堆結構的占比-XX:NewRatio
-
:設定新生代中-XX:SurvivorRatio
和Eden
空間的比例S0/S1
-
:設定新生代垃圾的最大年齡-XX:MaxTenuringThreshold
-
:輸出詳細的GC處理日志-XX:+PrintGCDetails
-
或-XX:+PrintGC
:列印gc簡要資訊-verbose:gc
-
:是否設定空間配置設定擔保-XX: HandlePromotionFailure
-XX: HandlePromotionFailure
說明:
在發生Minor GC之前,虛拟機會檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間。
- 如果大于,則此次Minor GC是安全的
- 如果小于,則虛拟機會檢視
-XX:HandlePromotionFailure
設定值是否允許擔保失敗。
➢如果
HandlePromotionFailure=true
, 那麼會繼續檢查老年代最大可用連續空間是否大于曆次晉升到老年代的對象的平均大小。
如果大于,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
如果小于,則改為進行一次Full GC。
➢如果HandlePromotionFailure=false, 則改為進行一次Full GC。
- 在JDK6之後(JDK7),
參數不會再影響到虛拟機的空間配置設定擔保政策,雖然源碼中還定義了HandlePromotionFailure
參數, 但是在代碼中已經不會再使用它。JDK6之後(JDK7)之後的規則變為隻要老年代的連續空間大于新生代對象總大小或者曆次晉升的平均大小就會進行Minor GC,否則将進行Full GC。HandlePromotionFailure
十二、堆小結
- 年輕代是對象的誕生、成長、消亡的區域,一個對象在這裡産生、應用,最後被垃圾回收器收集、結束生命。
- 老年代放置長生命周期的對象,通常都是從
區域篩(年齡計數器為15的對象)選拷貝過來的Java對象。Survivor
- 普通的對象可能會被配置設定在TLAB上;
- 如果對象較大,無法配置設定在 TLAB 上,則JVM會試圖直接配置設定在Eden其他位置上
- 如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接配置設定到老年代
- 當GC隻發生在年輕代中,回收年輕代對象的行為被稱為Minor GC。
- GC發生在老年代時則被稱為Major GC或者Full GC。
- 一般的,Minor GC的發生頻率要比Major GC高很多,即老年代中垃圾回收發生的頻率将大大低于年輕代。 (因為new的對象一般都配置設定在新生代, 新生代的對象都是朝生夕死的, 是以GC得頻率很高)
補充一、如何解決OOM ?
- 1、要解決OOM異常或heapspace的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是确認記憶體中的對象是否是必要的,也就是要先厘清楚到底是出現了
還是記憶體洩漏(MemoryLeak)
。記憶體溢出(Memory Overflow)
- 2、如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到
的引用鍊。于是就能找到洩漏對象是通過怎樣的路徑與GC Roots
相關聯并導緻垃圾收集器無法自動回收它們的。掌握了洩漏對象的類型資訊,以及GCRoots
引用鍊的資訊,就可以比較準确地定位出洩漏代碼的位置。GC Roots
- 3、如果不存在記憶體洩漏,換句話說就是記憶體中的對象确實都還必須存活着,那就應當檢查虛拟機的堆參數(
與-Xmx
) 與機器實體記憶體對比看是否還可以調大,從代碼上檢查是否存在某些對象生命周期過長、持有狀态時間過長的情況,嘗試減少程式運作期的記憶體消耗。-Xms