简介
本文大概分为以下几个部分:
在了解JVM内存分配的机制之前先来了解一下关于垃圾回收的几个术语:
- Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
接下来来了解一下JVM中对对象的分配机制。
对象优先在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。实例代码如下:
/**
* @Description: 对象优先在eden区分配
* -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -verbose:gc
* @Author: binga
* @Date: 2020/9/8 12:29
* @Blog: https://blog.csdn.net/pang5356
*/
public class EdenAllocateFirstTest {
public static final int _1M = 1024 * 1024;
public static void main(String[] args) {
byte[] a1, a2, a3;
a1 = new byte[2 * _1M];
a2 = new byte[2 * _1M];
}
}
这里通过-Xmn参数指定了年轻代的大小为10MB,同时指定Eden区与两个Servivor区的比例为8:1:1,那么Eden区为8MB,两个Servivor区大小为1MB。需要注意的是及时什么也不做也会占用大概2.24MB的空间。运行打印输入如下:
Heap
PSYoungGen total 9216K, used 6445K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc4b630,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3158K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
可以看到只有Eden区空间占用了78%,也就是6.24MB,其中a1和a2分别总共占用了4MB,而维持程序运行占用了2.24MB,都分配到了Eden区。接下来我们再为a3分配空间,如下:
public static void main(String[] args) {
byte[] a1, a2, a3;
a1 = new byte[2 * _1M];
a2 = new byte[2 * _1M];
// 为a3分配内存之前会进行一次Minor GC ,默认占用了2MB多的空间,同时a1和a2进入
// 老年代,Minor GC完后,将a3在eden区分配,可以看到eden占用了25%,也就是2MB
a3 = new byte[2 * _1M];
}
运行结果如下:
[GC (Allocation Failure) [PSYoungGen: 6281K->744K(9216K)] 6281K->4848K(19456K), 0.0046108 secs] [Times: user=0.03 sys=0.01, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 2931K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff822c40,0x00000000ffe00000)
from space 1024K, 72% used [0x00000000ffe00000,0x00000000ffeba020,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
Metaspace used 3267K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
从日志打印结果我们可以看到,进行一次GC,这是因为在a1和a2分配完后,Eden区剩余的空间不足2MB,不足以容纳a3,所以触发了Minor GC,但是这里可能会有一个疑问,就是为什么老年代的内存空间占用了40%,也就是4MB,这是因为在发生Minor GC后,a1和a2都还存活,但是一个Servivor的大小为1MB,不足以容纳a1和a2,所以在Minor GC完后,Survivor区放不下的对象直接放入老年代,但是Survivor仍占用了大约0.7MB的空间,就是上面说的JVM自己创建的一些对象,回收后还剩大约0.7MB,此时Eden去占用了大于2MB多,就是为a3分配的空间。
大对象直接进入老年代
一般地,Minor GC是对年轻代进行的垃圾回收,而在年轻代进行的垃圾回收流程是回收Eden区和其中一个Servivor(from)区,然后将存活的对象放入另外一个Servivor(to)区,当再次回收时回收Eden区和Servivor(to)区,然后放入另外一个Servivor(from)区,如此反复。在回收过程中存在占用空间较大的对象,复制的成本是比较高的,JVM针对该场景优化,设置了一个参数:-XX:PretenureSizeThreshold。用于限定对象的大小,当对象的大小占用空间超过这个阈值,则将对象直接在老年代分配,从而避免大对象在Minor GC时频繁的进行复制。来通过一个实例加深对该参数的理解。
/**
* @Description: 大对象直接进入老年代
* -verbose:gc -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728 -XX:+UseConcMarkSweepGC
* @Author: binga
* @Date: 2020/9/8 20:22
* @Blog: https://blog.csdn.net/pang5356
*/
public class BigObjectAllocateTest {
public static final int _1M = 1024 * 1024;
public static void main(String[] args) {
byte[] a1;
// 大对象直接在老年代分配
a1 = new byte[4 * _1M];
}
}
运行结果如下:
Heap
par new generation total 9216K, used 2349K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee4b610, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3267K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
通过日志可以看到,老年代占用了40%的空间,也就是4MB,当然年轻代也占用了大约2MB多的空间,这是程序运行占用的空间。需要说明的是
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,所以这里使用-XX:+UseConcMarkSweepGC参数指定老年代使用CMS垃圾收集器,那么默认的新生代会使用ParNew收集器。当你把
-XX:+UseConcMarkSweepGC这个参数去掉后JDK模式使用Parallel Scavenge,那么这个参数就失效了,a1会直接在Eden区分配(JDK1.8 默认使用的是Parallel Scavenge),日志如下:
heap
PSYoungGen total 9216K, used 6281K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 76% used [0x00000000ff600000,0x00000000ffc22630,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
Metaspace used 3254K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 355K, capacity 388K, committed 512K, reserved 1048576K
可以看到老年代的空间占用为0K。接下来增加上-XX:+UseConcMarkSweepGC这个参数并将
-XX:PretenureSizeThreshold设置为-XX:PretenureSizeThreshold=31457280,也就是30MB,再次进行测试,如下:
Heap
par new generation total 9216K, used 6445K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 78% used [0x00000000fec00000, 0x00000000ff24b620, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
concurrent mark-sweep generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3266K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
可以看到,此时Eden区占用了78%,而老年代则占用为0K。
长期存活的对象进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪 些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每 个对象一个对象年(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold 来设置。
接下来通过指定-XX:MaxTenuringThreshold=1和
-XX:MaxTenuringThreshold=15来测试,示例代码如下:
/**
* @Description: 长期存活的对象进入老年代
* -Xms60m -Xmx60m -Xmn20m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -verbose:gc -XX:PretenureSizeThreshold=20971520 -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=1
* @Author: binga
* @Date: 2020/9/8 20:35
* @Blog: https://blog.csdn.net/pang5356
*/
public class LongTermSurvivalTest {
public static final int _1M = 1024 * 1024;
public static void main(String[] args) {
byte[] a1, a2, a3;
// 2.88m
a1 = new byte[_1M / 4];
a2 = new byte[8 * _1M];
a3 = new byte[8 * _1M];
a3 = null;
a3 = new byte[8 * _1M];
}
}
首先将-XX:MaxTenuringThreshold=1运行结果如下:
[GC (Allocation Failure) [ParNew: 11087K->924K(18432K), 0.0102268 secs] 11087K->9118K(59392K), 0.0103192 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]
[GC (Allocation Failure) [ParNew: 9116K->0K(18432K), 0.0036165 secs] 17310K->9104K(59392K), 0.0036892 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 18432K, used 8301K [0x00000000fc400000, 0x00000000fd800000, 0x00000000fd800000)
eden space 16384K, 50% used [0x00000000fc400000, 0x00000000fcc1b670, 0x00000000fd400000)
from space 2048K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd600000)
to space 2048K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd800000)
concurrent mark-sweep generation total 40960K, used 9104K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3268K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
首先来看一下参数设置,Eden区为16MB,两个Survivor区分别为2MB,老年代为40MB。在为a3分配之前,Eden区空间已经不足以容纳a3,所有触发了一次Minor GC,通过日志可以看到第一次Minor GC完成后年轻代还有924K占用,而总内存占用9118K:
[GC (Allocation Failure) [ParNew: 11087K->924K(18432K), 0.0102268 secs] 11087K->9118K(59392K), 0.0103192 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]
所以a2不再年轻代,而是直接进入老年代,这是由于Survivor区放不下a2的原因,在第一Minor GC完后此时a1(当然还包括程序创建的一些对象)的分代年龄为1,那么将a3置为空并且再次分配时此时又触发了一下Minor GC,这里Minor GC完后年轻代的占用为0k:
[GC (Allocation Failure) [ParNew: 9116K->0K(18432K), 0.0036165 secs] 17310K->9104K(59392K), 0.0036892 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
这是因为a1及其他对象分代年龄超过了设置的阈值,直接晋升到了老年代,所以年轻代为0K,然后将a3再次分配,年轻代又有了8MB的占用空间。接下来将分代年龄的阈值设置为15,运行结果如下:
[GC (Allocation Failure) [ParNew: 11087K->922K(18432K), 0.0077247 secs] 11087K->9116K(59392K), 0.0078131 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [ParNew: 9114K->1193K(18432K), 0.0013936 secs] 17308K->9388K(59392K), 0.0014360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 18432K, used 9659K [0x00000000fc400000, 0x00000000fd800000, 0x00000000fd800000)
eden space 16384K, 51% used [0x00000000fc400000, 0x00000000fcc445e0, 0x00000000fd400000)
from space 2048K, 58% used [0x00000000fd400000, 0x00000000fd52a7f0, 0x00000000fd600000)
to space 2048K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd800000)
concurrent mark-sweep generation total 40960K, used 8194K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3268K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
通过这次的运行结果可以看到老年代只有a2占用的8MB空间,并且在第二次Minor GC完成后年轻代仍有空间占用:
[GC (Allocation Failure) [ParNew: 9114K->1193K(18432K), 0.0013936 secs] 17308K->9388K(59392K), 0.0014360 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象, 年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n及以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活 的对象,尽早进入老年代。对象动态年龄判断机制一般是在Minor GC之后触发的。来通过示例来查看:
/**
* @Description: 动态年龄判断
* -Xms60m -Xmx60m -Xmn20m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -verbose:gc -XX:MaxTenuringThreshold=15 -XX:+UseConcMarkSweepGC
* @Author: binga
* @Date: 2020/9/9 12:29
* @Blog: https://blog.csdn.net/pang5356
*/
public class DynamicAgeJudgmentTest {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
// 2.88M
byte[] a1, a2, a3, a4;
a1 = new byte[_1MB / 4];
a2 = new byte[_1MB / 4];
a3 = new byte[8 * _1MB];
a4 = new byte[8 * _1MB];
a4 = null;
a4 = new byte[8 * _1MB];
}
}
这里年轻代的收集还是使用的ParNew,其中Eden区空间为16MB,两个Survivor区占用2MB的空间,运行输出日志如下:
[GC (Allocation Failure) [ParNew: 11343K->1177K(18432K), 0.0093370 secs] 11343K->9371K(59392K), 0.0094320 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 9369K->0K(18432K), 0.0030890 secs] 17564K->9360K(59392K), 0.0031242 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
Heap
par new generation total 18432K, used 8301K [0x00000000fc400000, 0x00000000fd800000, 0x00000000fd800000)
eden space 16384K, 50% used [0x00000000fc400000, 0x00000000fcc1b670, 0x00000000fd400000)
from space 2048K, 0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd600000)
to space 2048K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd800000)
concurrent mark-sweep generation total 40960K, used 9360K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3268K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
可以通过内存占用情况知道,Eden区占用50%是a4的第二次分配占用的空间,而Survivor区占用为0%,通过收集日志知道:
[GC (Allocation Failure) [ParNew: 11343K->1177K(18432K), 0.0093370 secs] 11343K->9371K(59392K), 0.0094320 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
第一次Minor GC完之后,年轻代占用空间为1177k,包括a1和a2总共大约1MB,而a3在第一次Minor GC后直接进入了老年代(Survivor区放不下)。在第二次Minor GC后a1和a2及其他的对象分代年龄相同,并且占用空间大于一个Survivor区的50%,所以对象直接进入了老年代,这可以解释Survivor去可以容下a1和a2等对象,但最终Survivor区占用为0。那么将其中a2创建注释,查看日志结果如下:
[GC (Allocation Failure) [ParNew: 11087K->929K(18432K), 0.0074029 secs] 11087K->9123K(59392K), 0.0074829 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 9121K->1140K(18432K), 0.0009456 secs] 17315K->9334K(59392K), 0.0009884 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 18432K, used 9606K [0x00000000fc400000, 0x00000000fd800000, 0x00000000fd800000)
eden space 16384K, 51% used [0x00000000fc400000, 0x00000000fcc445e0, 0x00000000fd400000)
from space 2048K, 55% used [0x00000000fd400000, 0x00000000fd51d378, 0x00000000fd600000)
to space 2048K, 0% used [0x00000000fd600000, 0x00000000fd600000, 0x00000000fd800000)
concurrent mark-sweep generation total 40960K, used 8194K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3268K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
通过日志可以看到,当将a2的分配注释后,则老年代只有a3的8MB,因为第一次Minor GC后年轻代只剩余了929k,达不到Survivor其中一个区的50%。所以a1等对象仍然在年轻代,没有进入老年代:
[GC (Allocation Failure) [ParNew: 11087K->929K(18432K), 0.0074029 secs] 11087K->9123K(59392K), 0.0074829 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
至于第二次Minor GC后年轻代占用为1140k没有触发动态年龄判断,是因为不是同一个年龄代的对象。
Minor GC后存活的对象Survivor区放不下
该种情况在之前的实例中多次遇到过,这里不再演示该中情况。
老年代空间分配担保机制
年轻代每次Minor GC之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象), 就会看一个-XX:-HandlePromotionFailure(jdk1.8默认就设置了)的参数是否设置,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放 新的对象就会发生"OOM" 。当然,如果Minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有空间放Minor GC之后的存活对象,则也会发生“OOM”。
注意:老年代空间担保机制是在每次minor gc之前进行的判断。
流程如下:

结尾
感谢看到最后的朋友,都看到最后了点个赞再走吧,菜菜子在这里谢谢大家了,如有不对之处还请多多指正。
网友评论