JVM
- 堆
JDK
版本:1.8
堆内存针对于JVM
进程是唯一的,也就是一个进程只有一个JVM
,一个进程下会存在多个线程,这些线程共享同一个堆空间,其中还可以被划分为线程私有的缓冲区(Thread Local Allocation Buffer TLAB
)。堆空间是Java
内存管理的核心区域。
Java
堆区在JVM
启动时被创建,其空间大小同时也会被确定。堆空间是JVM
管理的最大一块内存空间,同时堆内存空间也是可配置的。
Java
虚拟机规范中规定,对可以处于物理上连续不断的内存空间中,但是逻辑上它应该被视为连续的。所有的对象实例以及数组都应当在运行时分配在堆空间上。The heap is the run-time data area from which memory for all class instances and arrays is allocated
。数组和对象可能永远都不会存储在栈上,在栈帧中保存的是地址引用,这个地址引用指向对象或者数组在堆空间中的位置。在一个方法执行结束后,堆中的对象不会马上被移除,只有在进行垃圾回收的时候才会被移除。堆是GC(Garbage Collection)
执行垃圾回收的重点区域。
JDK7
堆空间内部结构Java 7
及之前堆的内存逻辑上分为三个部分:
Young Generation Space
):其中新生区又被细分为Eden
区和Survivor
区。Tenure generation space
):老年代。Permanent Space
)JDK8
堆空间内部结构Java 8
及以后堆的内存逻辑上分为三个部分:
Young Generation Space
):其中新生区又被细分为Eden
区和Survivor
区。Tenure generation space
):老年代。Meta Space
)OOM
JVM
堆区用于存储Java
对象实例,在JVM
启动时堆空间大小就会被确定下来,可以通过-Xmx
和-Xms
选项进行配置堆空间大小。
-Xms
:表示堆区的起始内存,等价于-XX:InitialHeapSize
。-Xmx
:表示堆区的最大内存,等价于-XX:MaxHeapSize
。一旦堆区的内存大小超过-Xmx
所指定的最大内存时,将会抛出OutOfMemoryError
错误。通常情况下会将-Xms
与-Xmx
两个选项配置成相同的参数值,这样做能够在JVM
进行垃圾回收清理完毕堆区后不需要重新分隔计算堆区的大小从而达到提升性能的目的。
在默认情况下:
-Xms
:物理内存大小 / 64。-Xmx
:物理内存大小 / 4。JVM
堆中存储的Java
对象可以被划分为两类:
JVM
的生命周期保持一致。JVM
堆空间被细分为年轻代(Young Gen
)和老年代(Old Gen
)。其中年轻代又被划分为Eden
区、Survivor 0
区和Surivior 1
区,其中Survivor 0
区和Surivior 1
区由于会进行交换,在宏观上是一种相对而言的区域,所以又被称为from
区和to
区。
新生代与老年代的空间占比在实际开发中一般不会进行调整。
首先看一下新生代中的Eden
区和Survivor 0
区和Survivor 1
区之间的空间占比。
在Hotspot
虚拟机中,Eden
区和Survivor 0
区和Survivor 1
区之间的空间占比为:8:1:1
。可以通过选项-xx:SurvivorRatio
选项调整新生代中的空间占比,如:-xx:SurvivorRatio=8
。JVM
中几乎所有的Java
对象都是在Eden
区被new
出来的。新生代最大内存可通过-Xmn
选项进行配置,一般这个参数使用默认值即可。
新生代与老年代在堆中的空间占比。
在Hotspot
虚拟机中,新生代与老年代的空间占比为1:2
,这个比例可以通过-XX:NewRatio
选项进行配置。默认-XX:NewRatio=2
代表新生代的空间占比为1,老年代空间占比为2,新生代占整个堆空间的1 / 3。可以修改配置如:-XX:NewRatio=4
代表新生代空间占比为1,老年代占比为4,新生代占整个堆空间的1 / 5。
为新对象分配内存是一件非常严谨和复杂的任务,不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC
执行完内存回收后是否会在内存空间中产生内存碎片。
Java
中new
关键字创建的新对象先放在Eden
区。此区域有大小限制。Eden
区的空间被填满时,应用程序又需要创建新的Java
对象,JVM
的垃圾回收器将堆Eden
区进行垃圾回收(Minor GC
),将Eden
区中不再被其它对象所引用的对象进行销毁,销毁后再将新创建的Java
对象放到Eden
区。Eden
区中剩余对象移动至Survivor 0
区。Survivor 0
区中的对象,如果没有进行垃圾回收,则会将其复制到Survivor 1
区。Survivor 0
区,重复步骤4、5。反复经过15次之后,还存在的对象将会移动到老年区。重复步骤4、5的次数可以通过选型-Xx:MaxTenuringThreshold=N
进行配置,默认是15次。Major GC
)进行养老区内存清理。若养老区经过Major GC
之后,依然无法进行对象的保存,就会抛出OutOfMemory
异常。针对Survivor 0
区和Survivor 1
区之间的复制交换,通常都是将复制之后为空的一个移动到Survivor 1
区,即Survivor 0
区总是会存放对象的,这也是为什么说Survivor 0
和Survivor 1
区是相对而言的,谁空谁是to
区。
针对垃圾回收,频繁在新生代进行垃圾收集,很少在老年代进行。
Minor GC
、Major GC
、Full GC
JVM
的针对堆空间的垃圾回收大部分都是回收的新生代,针对Hotspot VM
的实现,GC
按照回收区范围又分为两大中类型
Partial GC
)
Minor GC / Young GC
):只是新生代的垃圾收集。Major GC / Old GC
):只是老年代的垃圾收集。
CMSGC
会有单独收集老年代的行为。Major GC
会和Full GC
混淆使用,需要具体辨别是老年代垃圾回收还是整堆回收。Mixed GC
):对整合新生代以及部分老年代进行垃圾收集。
G1 GC
会有这种行为。Full GC
):针对整个Java
堆和方法区的垃圾收集。GC
触发机制GC(Minor GC)
触发机制当年轻代空间不足时,就会触发Minor GC
,这里指的年轻代是Eden
区,Survivor
区空间不足不会引发GC
。每次Minor GC
都会清理年轻代内存。因为Java
对象大多都具备朝生夕灭的特性,所以在JVM
中Minor GC
是非常频繁的,垃圾回收速度也比较快。Minor GC
会引发STW
,当JVM
堆堆中的新生代进行Minor GC
时会暂停其它用户线程,等待垃圾回收结束用户线程才会被恢复。
GC(Major GC / Full GC)
触发机制Major GC
或Full GC
指发生在老年代的GC
,当对象从老年代消失时,就发生了Major GC
或Full GC
。
Major GC
经常会伴随至少一次的Minor GC
,但是这种情况并非绝对。在Parallel Scavenge
收集器的收集策略里就有直接进行Major GC
的策略选择过程。也就是说当老年代空间不足时,通常情况下会先进行一次Minor GC
,如果进行Minor GC
之后老年代空间还是不足则会触发Major GC
对老年代进行垃圾回收。
Major GC
的速度一般会比Minor GC
慢10倍以上,Major GC
引发的STW
时间则会比Minor GC
的STW
时间更长。如果在Major GC
之后堆空间内存还是不足则会抛出OutOfMemory
异常。
Full GC
触发机制触发整堆垃圾回收的场景有五种:
System.gc()
时,系统将会建议JVM
执行Full GC
,但是不一定会执行。Minor GC
后进入老年代的对象平均内存大小大于老年代当前可用的内存。Eden
区、Survivor Space 0(From Space)
区向Survivor Space 1(To Space)
区复制时,对象大小大于To Space
区可用内存则把该对象转存到老年代中,且当前老年代可用内存大小小于该对象大小。注意:Full GC
是开发或者调优中要尽最大努力避免的垃圾回收机制,Full GC
非常慢。
Java
中不同对象的生命周期是不同的,其中70% ~ 99%
的都是临时对象。Java
堆空间分配为新生代和老年代,其中新生代分为伊甸园区(Eden
)、幸存者0区(Survivor 0 / from
)、幸存者1区(Survivor 1 / to
),其中幸存者区空间大小相同,其中to
区总为空。而老年代中存放新生代中经过多次GC
后依然存活的Java
对象。
JVM
对堆空间进行分代的意义就是为了优化GC
的性能,可以理解为分治思想。如果没有分代,所有Java
对象都在一起,触发GC
之后需要寻找出没有使用的Java
对象,这样的话会进行全堆扫描,而Java
对象又有很大一部分是临时对象,所以将特定的对象放在一块特定的区域进行专治,针对不同代的Java
对象使用不同的垃圾回收机制进行垃圾回收处理,将会对JVM
触发GC
时的性能有很大的优化帮助。同时将堆空间分代之后,也便于对象堆内存的分配。
在Java
中新创建的对象,JVM
会在Eden
区中创建该对象,经历过一次Minor GC
之后仍然存活并且能够被Survivor 0
区容纳,该对象将会被移动到Survivor 0
区中并将该对象的年龄设置为1。该对象每在Survivor
区中熬过一次Minor GC
其年龄就会增加1岁,当该对象的年龄到达一定的程度之后(Hotspot
默认为15岁,不同的JVM
会有不同的默认值)该对象就会被移动到老年代。
针对不同年龄段的Java
对象JVM
的堆内存分配策略如下:
Eden
区。Survivor
区中相同年龄的所有对象大小之和大于Survivor
区空间的一半,年龄大于等于该年龄的对象可直接分配到老年代,无需等待这些对象的年龄到达MaxTenuringThreshold
选项中设置的阈值。Java
对象堆空间分配担保:-XX:HandlePromotionFailure
。TLAB
Thread Local Allocation Buffer
从JVM
内存模型的角度出发,对堆内存中的Eden
区继续进行划分,JVM
为每个线程都分配了一个私有缓存区域,它包含在Eden
空间内。在多线程同时在堆空间中为Java
对象分配堆内存时,使用TLAB
来避免一系列的非线程安全问题,同时还能够提升堆内存分配的吞吐量,这种内存分配方式称之为快速分配策略。
Open JDK
衍生的JVM
都提供了TLAB
的设计:
在应用程序中,开发者可通过-XX:UseTLAB
选项设置是否开启TLAB
空间。默认情况下TLAB
占Eden
空间的内存非常小,仅仅占整合Eden
区空间的1%
。开发者可以通过-XX:TLABWasteTargetPercent
选项配置TLAB
空间所占用Eden
空间的百分比大小。
设置开启TLAB
之后,在创建新的Java
对象时,首先会尝试在TLAB
中进行堆内存分配,JVM
也确实将TLAB
作为分配堆内存的首先。一旦该对象在TLAB
空间中分配堆内存失败,JVM
会立即尝试通过使用加锁机制确保操作数据的原子性进而直接在Eden
空间中为该对象分配堆内存。
JVM
堆空间参数选项部分参数说明:
参数选项 | 说明 |
---|---|
-XX:+PrintFlagsInitial |
查看JVM 所有的参数选项的默认初始值 |
-XX:+PrintFlagsFinal |
查看JVM 所有的参数的最终值(可能会存在修改,不再是初始值) |
-Xms |
堆空间最小内存(默认为物理内存的1 / 64 ) |
-Xmx |
堆空间最大内存(默认为物理内存的1 / 4 ) |
-XX:NewRatio |
新生代与老年代在堆结构的占比(默认为1:2 ) |
-XX:SurvivorRatio |
新生代中Eden 和S0 / S1 空间的比例(默认为8:1:1 ) |
-XX:MaxTenuringThreshold |
新生代垃圾对象的最大年龄 |
-XX:+PrintGCDetails |
输出详细的GC 处理日志 |
-Xx:+PrintGC / -verbose:gc |
打印GC 简要信息 |
-XX:HandlePromotionFalilure |
是否设置空间分配担保 |
触发Minor GC
时,在正真执行Minor GC
之前,JVM
虚拟机会检查老年代中最大可用的连续空间是否大于新生代所有对象的总空间。
Minor GC
是安全的。JVM
虚拟机首先会查看-XX:HandlePromotionFalilure
选项是否配置允许空间分配失败担保。
-XX:HandlePromotionFalilure=true
,JVM
会继续检查老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小。
JVM
会尝试进行一次Minor GC
,但这次Minor GC
仍然是有风险的。JVM
会直接进行一次Full GC
。-XX:HandlePromotionFalilure=true
,JVM
会直接进行一次Full GC
。在JDK6 Update 24
之后,-XX:HandlePromotionFalilure
选项不会再影响虚拟机的空间分配担保策略,虽然Open JDK
中仍然还是定义了-XX:HandlePromotionFalilure
选项,但是在代码层面已经完全没有使用这个参数选项了。JDK6 Update 24
之后的规则变更为:只要老年代的最大连续可用空间大于新生代对象总大小或者历次晋升到老年代的对象的平均大小JVM
就会进行Minor GC
,否则将进行Full GC
。
Oracle JDK Document Addres
:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
在《深入理解
Java虚拟机》
第3
版第45
页抬头的段落尾写道:
随着Java
语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大、栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java
对象实例都分配在堆上页渐渐变得不是那么绝对了。
有一种特殊情况,如果经过逃逸分析(Escape Analysis
)后发现一个对象并没有逃逸出方法的话,那么该对象就可能被优化成栈上分配,这样就无需在堆上进行内存分配,JVM
也就无需针对此类临时对象进行垃圾回收,这也是最常见的堆外存储技术。
基于Open JDK
深度定制的TaboBaoVM
,其中创新的GCIH(Garbage Collection Invisible Heap)
技术实现off-heap
,将生命周期较长的Java
对象从Heap
中移动至Heap
外,并且GC
不能管理GCIH
内部的Java
对象,以此达到降低GC
的回收频率和提升GC
的回收效率的目的。