目录

一、堆的概述

二、堆内存划分

三、设置堆内存大小与OOM

四、年轻代与老年代

五、图解对象分配过程

六、总结


一、堆的概述

堆是JVM运行时数据区最重要的一部分,是JVM管理的最大的一块内存空间,同时也是垃圾回收的重要区域。堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,一个进程包含多个线程,所以同一个进程中的多个线程是共享一个堆空间的

Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,并且堆内存的大小是可以动态调节的【通过-Xms和-Xmx参数进行调整】。

《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上(理论上是全部对象,但是JVM优化,可能存在栈上分配)。如下图:

cnn空间信息 空间信息是什么意思_运行时数据区

二、堆内存划分

Java 7及之前堆内存逻辑上分为三部分:新生代 + 老年代 + 永久区

  • Young Generation Space: 新生区 Young/New, 又被划分为Eden区和Survivor区;
  • Tenure generation space :老年代 Old/Tenure;
  • Permanent Space:永久区;

cnn空间信息 空间信息是什么意思_堆_02

Java 8及之后堆内存逻辑上分为三部分:新生代 + 老年代 + 元空间

  • Young Generation Space: 新生区 Young/New, 又被划分为Eden区和Survivor区;
  • Tenure generation space :老年代 Old/Tenure;
  • Meta Space : 元空间,使用本地内存;

堆空间内部结构,JDK1.8以后使用元空间替换永久代,如下图:

cnn空间信息 空间信息是什么意思_运行时数据区_03

三、设置堆内存大小与OOM

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,我们可以通过JVM参数"-Xmx"和"-Xms"来进行调整堆空间大小。

  • -Xms:设置堆区的初始内存,等价于-xx:InitialHeapSize;
  • -Xmx:设置堆区的最大内存,等价于-XX:MaxHeapSize;

一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutofMemoryError内存溢出异常。

通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分配计算堆区的大小,从而提高性能。

默认情况下,初始内存大小:物理电脑内存大小 / 64;最大内存大小:物理电脑内存大小 / 4;

public class HeapSpaceTest {
    public static void main(String[] args) throws InterruptedException {
        // 返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        // 返回Java虚拟机试图使用的最大堆内存
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        System.out.println("-Xms:" + initialMemory + "M");
        System.out.println("-Xmx:" + maxMemory + "M");
        
        //模拟休眠,方便查看内存分配情况
        Thread.sleep(200000);
    }
}

运行结果如下:

-Xms:236M
-Xmx:3499M

然后我们设置一下堆空间大小,使用-Xms和-Xmx设置,设置初始堆和最大堆大小都是600m:

cnn空间信息 空间信息是什么意思_JVM_04

控制台输出:

-Xms:575M
-Xmx:575M

可以看到,我们指定的堆大小明明是600m,为什么输出来的只有575m?分析如下。

下面我们介绍一下如何查看堆内存的内存分配情况,有两种方式:

  • 第一种方式:jps -> jstat -gc 进程id;

cnn空间信息 空间信息是什么意思_cnn空间信息_05

  • jps:查看Java进程信息;
  • jstat:查看JVM在GC时的统计信息;

下面是几个重要字段代表的意思:

  • S0C:survivor幸存者0区的总大小;
  • S1C:survivor幸存者1区的总大小;
  • S0U:survivor幸存者0区的已使用大小;
  • S1U:survivor幸存者1区的已使用大小;
  • EC:Eden区域总大小;
  • EU:Eden区域已使用大小;
  • OC:老年代总大小;
  • OU:老年代已使用大小;

那么堆空间 = 老年代大小 + 新生代大小 = OC + (EC + S0C + S1C) = 409600 + (153600 + 25600 + 25600) = 614400M,然后614400 / 1024 = 600M,刚好是我们设置的JVM参数中堆大小【600M】。

那为什么前面的程序运行结果输出只有575M呢,原因是新生代中的survivor幸存者0区、survivor幸存者1区永远都是其中一个为空,一个不为空【也叫from、to区域,to区域永远都是空的】。所以前面的程序就是:

堆空间 = OC + (EC + S0C【 或者S1C,只加一个】) = 409600 + (153600 + 25600) = 588800M,然后588800 / 1024 =575M,刚好跟运行结果对应上。

  • 第二种方式:加上JVM启动参数打印程序GC日志 => -XX:+PrintGCDetails;

cnn空间信息 空间信息是什么意思_虚拟机_06

观察程序输出:

cnn空间信息 空间信息是什么意思_运行时数据区_07

 同样,小伙伴们可自行计算一下,跟前面我们JPS方式计算出来结果是一样的。下面看一个OutOfMemory内存溢出的例子:

public class OOMTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        while (true) {
            list.add(999999999);
        }
    }
}

可以看到,我们通过死循环,不断地往集合中存入数据,然后设置启动参数-Xms10m -Xmx10m,然后运行程序,结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at com.wsh.rocketmq.rocketmqdemo.OOMTest.main(OOMTest.java:10)

运行后,就出现OOM了,可见,当堆空间大小超过我们设置的-Xmx指定的最大堆空间大小【这里是10m】时,表示堆已经无法分配空间给对象了,于是就报OOM内存溢出了。

四、年轻代与老年代

存储在JVM中的Java对象可以被划分为两类:

  • 生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;
  • 生命周期非常长的对象,在某些极端的情况下还能够与JVM的生命周期保持一致;

Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen),其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)。

cnn空间信息 空间信息是什么意思_JVM_08

其中年轻代又分为Eden、S0、S1三块,Eden:From:to大小比例默认是8:1:1,新生代:老年代 比例为1 : 2。

cnn空间信息 空间信息是什么意思_虚拟机_09

Eden:8 / 10 From:1 / 10 To:1 / 10

Young:Old = 1 :2

我们可以通过-XX:NewRatio参数配置新生代与老年代在堆结构的占比,默认-XX:NewRatio=2,表示新生代占1,老年代占2,即新生代占整个堆的1/3。当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优【适当增大老年代空间大小】。

在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1,可以通过参数-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=9。

几乎所有的Java对象都是在Eden区被new出来的,绝大部分的Java对象的销毁都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)

五、图解对象分配过程

为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

  • new的对象先放Eden区,此区有大小限制;
  • 当Eden区的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对Eden区进行垃圾回收(MinorGC),将Eden区中的不再被其他对象所引用的对象进行销毁,再加载新的对象放到Eden区;
  • 然后将Eden区的剩余对象移动到幸存者0区(S0);
  • 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区(S0)的,如果没有回收,就会放到幸存者1区(S1);
  • 如果再次经历垃圾回收,此时会重新放回幸存者0区(S0),接着再去幸存者1区(S1);
  • 啥时候能去老年代呢?默认是经历15次,可以通过参数调整对象分代年龄【-XX:MaxTenuringThreshold= N进行设置】;
  • 当老年代内存不足时,再次触发Major GC,进行老年代的内存清理;
  • 若老年代执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM异常;

我们创建的对象,一般都是存放在Eden区的,当我们Eden区满了后,就会触发GC操作,一般被称为 YGC / Minor GC操作。

cnn空间信息 空间信息是什么意思_JVM_10

当我们进行一次垃圾收集后,红色的将会被回收,而绿色的还会被占用着,存放在S0(Survivor From)区。同时我们给每个对象设置了一个年龄计数器,一次回收后就是1。

同时Eden区继续存放对象,当Eden区再次存满的时候,又会触发一个MinorGC操作,此时GC将会把 Eden和Survivor From中的对象进行一次收集,把存活的对象放到 Survivor To区,同时让年龄 + 1。

cnn空间信息 空间信息是什么意思_JVM_11

 我们继续不断的进行对象生成 和垃圾回收,当Survivor中的对象的年龄达到15的时候,将会触发一次 Promotion晋升的操作,也就是将年轻代中的对象晋升到老年代中。

cnn空间信息 空间信息是什么意思_堆_12

特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作。

如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。

不过对象分配也不一定完全按照上述讲的那样去分配,其中可能存在一些对象直接进入老年代的现象。如下图是对象分配的特殊情况: 

cnn空间信息 空间信息是什么意思_cnn空间信息_13

六、总结

  • 堆空间是JVM运行时数据区最大的一块区域,也是GC垃圾回收的重要区域,可以通过-Xms和-Xmx设置堆空间大小;
  • 堆分为新生代 + 老年代 + 元空间(1.8之后),其中新生代可分为Eden + From(s0) + To(s1) ;
  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to;
  • 在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作;
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不在永久代和元空间进行收集;
  • 新生代采用复制算法的目的:是为了减少内存碎片;