6 堆 Heap??????????
6.1 核心概述
- 一個(gè)JVM實(shí)例只存在一個(gè)堆內(nèi)存,堆也是Java內(nèi)存管理的核心區(qū)域。
- Java 堆區(qū)在JVM啟動(dòng)的時(shí)候即被創(chuàng)建,其空間大小也就確定了。是JVM管理的最大一塊內(nèi)存空間。
- 堆是GC(Garbage Collection,垃圾收集器)執(zhí)行垃圾回收的重點(diǎn)區(qū)域。
堆內(nèi)存細(xì)分
Java 7及之前堆內(nèi)存邏輯上分為三部分:新生區(qū) + 養(yǎng)老區(qū) + 永久區(qū)
- Young Generation Space 新生區(qū) Young/New
- 又被劃分為Eden區(qū)和Survivor區(qū)
- Tenure generation space 養(yǎng)老區(qū) Old/Tenure
- Permanent Space 永久區(qū) Perm
Java 8及之后堆內(nèi)存邏輯上分為三部分:新生區(qū) + 養(yǎng)老區(qū) + 元空間
- Young Generation Space 新生區(qū) Young/New
- 又被劃分為Eden區(qū)和Survivor區(qū)
- Tenure generation space 養(yǎng)老區(qū) Old/Tenure
- Meta Space 元空間 Meta
約定:新生區(qū)(代)<=>年輕代 、 養(yǎng)老區(qū)<=>老年區(qū)(代)、 永久區(qū)<=>永久代
6.2 設(shè)置堆內(nèi)存大小與OOM
堆空間大小設(shè)置
Java堆區(qū)用于存儲Java對象實(shí)例,堆的大小在JVM啟動(dòng)時(shí)就已經(jīng)設(shè)定好了,大家可以通過選項(xiàng)"-Xmx"和"-Xms"來進(jìn)行設(shè)置。
通常會將-Xms和-Xmx兩個(gè)參數(shù)配置相同的值,其目的是為了能夠在 Java垃圾回收機(jī)制清理完堆區(qū)后不需要重新分隔計(jì)算堆區(qū)的大小,從而提高性能。
默認(rèn)情況下
- 初始內(nèi)存大小:物理電腦內(nèi)存大小 / 64
- 最大內(nèi)存大小:物理電腦內(nèi)存大小 / 4
public class HeapTest {
? ?public static void main(String[] args) {
? ? ? ?// Java虛擬機(jī)中的堆內(nèi)存容量
? ? ? ?long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
? ? ? ?// Java虛擬機(jī)中的最大堆內(nèi)存容量
? ? ? ?long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
?
? ? ? ?System.out.println("-Xms: " + initialMemory + "M");
? ? ? ?System.out.println("-Xmx: " + maxMemory + "M");
? }
}
查看設(shè)置的參數(shù)
- 方式1:jps / jstat -gc 進(jìn)程id
- 方式2:-XX:+PrintFCDetails
OutOfMemory舉例
public class OOMTest {
? ?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)));
? ? ? }
? }
}
打印結(jié)果
Exception in thread "main" java.lang.OutofMemoryError: Java heap space
? ?at com.atguigu. java.Picture.<init>(OOMTest. java:25)
? ?at com.atguigu.java.O0MTest.main(OOMTest.java:16)
6.3 年輕代與老年代
存儲在JVM中的Java對象可以被劃分為兩類:
- 一類是生命周期較短的瞬時(shí)對象,這類對象的創(chuàng)建和消亡都非常迅速
- 另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致
- 默認(rèn)
-XX:NewRatio=2
,表示新生代占1,老年代占2,新生代占整個(gè)堆的1/3
幾乎所有的 Java對象都是在Eden區(qū)被new出來的。絕大部分的Java對象的銷毀都在新生代進(jìn)行了。
6.4 圖解對象分配過程
1. new的對象先放伊甸園區(qū)。此區(qū)有大小限制。
2. 當(dāng)伊甸園的空間填滿時(shí),程序又需要?jiǎng)?chuàng)建對象,JVM的垃圾回收器將對伊甸園區(qū)進(jìn)行垃圾回收(MinorGC),將伊甸園區(qū)中的不再被其他對象所引用的對象進(jìn)行銷毀。再加載新的對象放到伊甸園區(qū)。
3. 然后將伊甸園中的剩余對象移動(dòng)到幸存者0區(qū)。
4. 如果再次觸發(fā)垃圾回收,此時(shí)上次幸存下來的放到幸存者0區(qū)的,如果沒有回收,就會放到幸存者1區(qū)。
5. 如果再次經(jīng)歷垃圾回收,此時(shí)會重新放回幸存者0區(qū),接著再去幸存者1區(qū)。
?
6. 啥時(shí)候能去養(yǎng)老區(qū)呢?可以設(shè)置次數(shù)。默認(rèn)是15次。
○ 可以設(shè)置參數(shù):進(jìn)行設(shè)置-Xx:MaxTenuringThreshold= N
7. 在養(yǎng)老區(qū),相對悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),再次觸發(fā)GC:Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理
8. 若養(yǎng)老區(qū)執(zhí)行了Major GC之后,發(fā)現(xiàn)依然無法進(jìn)行對象的保存,就會產(chǎn)生OOM異常。
-
伊甸園區(qū)的對象先往to區(qū)放(空的)
-
年齡計(jì)數(shù)器達(dá)到15晉升老年代
-
總結(jié)
- 針對幸存者s0,s1區(qū)的總結(jié):復(fù)制之后有交換,誰空誰是to
- 關(guān)于垃圾回收:頻繁在新生區(qū)收集,很少在老年代收集,幾乎不再永久代和元空間進(jìn)行收集
流程圖
常用調(diào)優(yōu)工具(在JVM下篇:性能監(jiān)控與調(diào)優(yōu)篇會詳細(xì)介紹)
- JDK命令行
- Eclipse:Memory Analyzer Tool
- Jconsole
- VisualVM
- Jprofiler
- Java Flight Recorder
- GCViewer
- GC Easy
6.5 Minor GC、MajorGC、Full GC
JVM在進(jìn)行GC時(shí),并非每次都對上面三個(gè)內(nèi)存區(qū)域一起回收的,大部分時(shí)候回收的都是指新生代。
針對Hotspot VM的實(shí)現(xiàn),它里面的GC按照回收區(qū)域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(FullGC)
- 部分收集:不是完整收集整個(gè)Java堆的垃圾收集。其中又分為:
-
- 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
-
- 老年代收集(Major GC / Old GC):只是老年代的圾收集。
-
-
- 目前,只有CMSGC會有單獨(dú)收集老年代的行為。
-
-
-
- 注意,很多時(shí)候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收。
-
-
- 混合收集(MixedGC):收集整個(gè)新生代以及部分老年代的垃圾收集。
-
-
- 目前,只有G1 GC會有這種行為
-
- 整堆收集(Full GC):收集整個(gè)java堆和方法區(qū)的垃圾收集。
6.6 堆空間分代思想
分代的唯一理由就是優(yōu)化GC性能。
如果沒有分代,GC的時(shí)候要找到哪些對象沒用,就會對堆的所有區(qū)域進(jìn)行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創(chuàng)建的對象放到某一地方,當(dāng)GC的時(shí)候先把這塊存儲“朝生夕死”對象的區(qū)域進(jìn)行回收,這樣就會騰出很大的空間出來。
6.7 內(nèi)存分配策略
針對不同年齡段的對象分配原則如下所示:
- 優(yōu)先分配到Eden
- 大對象直接分配到老年代(盡量避免程序中出現(xiàn)過多的大對象)
- 長期存活的對象分配到老年代
- 動(dòng)態(tài)對象年齡判斷:如果survivor區(qū)中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進(jìn)入老年代,無須等到
MaxTenuringThreshold
中要求的年齡。 - 空間分配擔(dān)保:
-XX:HandlePromotionFailure
6.8 為對象分配內(nèi)存:TLAB
6.9 小結(jié):堆空間的參數(shù)設(shè)置
// 詳細(xì)的參數(shù)內(nèi)容會在JVM下篇:性能監(jiān)控與調(diào)優(yōu)篇中進(jìn)行詳細(xì)介紹,這里先熟悉下
-XX:+PrintFlagsInitial ?//查看所有的參數(shù)的默認(rèn)初始值
-XX:+PrintFlagsFinal ?//查看所有的參數(shù)的最終值(可能會存在修改,不再是初始值)
-Xms ?//初始堆空間內(nèi)存(默認(rèn)為物理內(nèi)存的1/64)
-Xmx ?//最大堆空間內(nèi)存(默認(rèn)為物理內(nèi)存的1/4)
-Xmn ?//設(shè)置新生代的大小。(初始值及最大值)
-XX:NewRatio ?//配置新生代與老年代在堆結(jié)構(gòu)的占比
-XX:SurvivorRatio ?//設(shè)置新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold ?//設(shè)置新生代垃圾的最大年齡
-XX:+PrintGCDetails //輸出詳細(xì)的GC處理日志
//打印gc簡要信息:①-Xx:+PrintGC ② - verbose:gc
-XX:HandlePromotionFalilure://是否設(shè)置空間分配擔(dān)保
堆是分配對象的唯一選擇么?
在Java虛擬機(jī)中,對象是在Java堆中分配內(nèi)存的,這是一個(gè)普遍的常識。
但是,有一種特殊情況,那就是如果經(jīng)過逃逸分析(Escape Analysis)后發(fā)現(xiàn),一個(gè)對象并沒有逃逸出方法的話,那么就可能被優(yōu)化成棧上分配。這樣就無需在堆上分配內(nèi)存,也無須進(jìn)行垃圾回收了。這也是最常見的堆外存儲技術(shù)。