Java虚拟机中的内存分配

一、堆空间的划分和JVM相关参数的解释

Java技术体系中所提倡的自动内存管理最终可以归结为自动化的解决下面两个问题:

  • 给对象分配内存空间
  • 回收分配给对象的内存

先来看一下在Java 8 之后堆内存在逻辑上的划分:

  1. 新生代(新生区):PSYoungGen (又分为Eden、from、to)
  2. 老年代(养老区):ParOldGen
  3. 元空间:MetaSpace

一起来看看给对象在内存分配空间的过程:

  1. 对象优先在堆内存的Eden区中分配
  2. 在后续分配新对的空间时,Eden区如果满了会进行Minor GC,还存活的就会进入新生代中的Survivor区其中的一个
  3. 如果再次经历垃圾回收时,会将已经存放有数据的幸存者区(Survivor)中的内存区(一般把已经存放了数据的称为from区,空的称为to区)通过复制,全部转移到to区中(会重复此操作)
  4. 在对象经历一定次数的Minor GC后,就会进入老年代(还有其他特殊的情况也会直接进入老年代)

设置堆内存大小

Java堆区用于存储Java对象实例,堆的大小在JVM启动的时候就已经设置好了,先来看看默认情况下的内存大小。

默认情况下:

  • 初始内存大小:物理电脑内存大小的64分之一(例如我的电脑内存16G,则16 / 64 = 0.25 G)
  • 最大内存大小:物理电脑内存大小的四分之一(例如我的电脑内存16G,则16 / 4 = 4 G)

可以设置堆空间(新生代+老年代)的初始内存大小:-Xms
在这里插入图片描述

  • -X是JVM的运行参数
  • ms是memory start

可以设置堆空间的最大内存大小: - Xmx
在这里插入图片描述
通过下面的代码可以获取默认的初始内存和最大内存的大小

package learn.demo.jvm;

public class HeadSpaceInitial {
    
    
    public static void main(String[] args) {
    
    
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long maxMemory = Runtime.getRuntime().maxMemory()/ 1024 / 1024;
        System.out.println("-Xms:"+initialMemory+"M");
        System.out.println("-Xms:"+maxMemory+"M");
    }
}

运行结果:(默认情况)
在这里插入图片描述
通过设置:-Xms20m -Xmx20m
在这里插入图片描述
运行结果:
在这里插入图片描述
可配置新生代和老年代在对结构中的占比:

  • 默认:-XX:NewRatio=2,即堆内存分为三份,新生代占1,老年代占2,新生代栈整个堆的1/3
  • 可修改 -XX:-XX:NewRatio=4
    在这里插入图片描述
    在HotSpot中,Eden空间和另外两个Survivor空间缺省所占比例是 8:1:1
    我们可通过:-XX:SurvivorRetio 来调整这个空间比例
    比如:-XX:SurvivorRetio=4
    在这里插入图片描述

在JVM中的内存对象可以划分为两类:

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

二、对象在堆内存的内存分配过程

1、对象优先在堆内存的新生代中Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden没有足够的空间进行分配的时候,就会发起一次Minor GC。

虚拟机中提供了 -XX:+PrintGCDetails 这个收集器日志参数,运行的时候配置该参数,虚拟机就会在发生垃圾回收行为的时候打印内存回收日志,同时在进程退出结束的时候输出当前的内存各区域的分配情况。
代码:

public class TestPrintGC {
    
    

    public final static int ONE_MB = 1024*1024;
    public static void main(String[] args) {
    
    
        byte[] byteArray1, byteArray2, byteArray3, byteArray4;
        byteArray1 = new byte[2*ONE_MB];
        byteArray2 = new byte[2*ONE_MB];
        byteArray3 = new byte[2*ONE_MB];
        byteArray4 = new byte[4*ONE_MB];

    }
}

运行时配置的参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
参数的解释说明:

  • -Xms20m :限制的Java的最小内存(起始内存)为20MB
  • -Xmx20m:限制Java堆的最大内存为20MB,当最小内存和最大内存相等时,代表该JVM进程的内存不可拓展
  • -Xmn10m:表示分配给新生代的内存时10MB,说明剩下的10M分配给老年代,
  • -XX:SurvivorRatio=8:决定了新生代中Eden区与一个Survivo区的空间比例时 8:1

运行结果:
在这里插入图片描述

从运行结果来看:

  • 发生了一次在给上面的程序中分配 byteArray4 (该对象4MB)的时候,发生了一次Minor GC,在这次GC的结果是:新生代中7580K变成了1000K,新生代的总容量(9216K,是Eden区+一个Survivor 区)

  • 总内存占有量却就几乎不变(因byteArray1、byteArray2、byteArray3三个数组对象是存活的,虚拟机并没找到可回收e对象)。

  • 触发 Minor GC的原因,前三个对象的大小总和是6M,加上待分配 的byteArray4(4M),这样超过新生代的中Eden中的8192K,所以触发了Eden的垃圾回收机制,进行一次Minor GC

  • 在MInor GC期间,虚拟机发现Eden中的三个对象大小都是2M,无法存放进入Survivor空间(因为Survivor空间的大小只有1MB),为了新生代中的Eden区能够存放下byteArray4 ,所以通过分配担保机制提前转移到老年代区

  • 从上面看,堆内存中除了我们创建的数组,还有其他的数据,例如在经过一次的垃圾回收机制后,老年代的数据是 7392K,但是我们存放的三个数组是2M的,总共6MB应该是6144K;还有在新生代的Eden区中的数据是5240K,我们存放的
    byteArray4是4MB的,从这些说明JVM在运行的时候,除了我们创建的一些数据,JVM自己也会产生一定的数据。

2、(特例)大对象直接进入老年代

所谓的大对象是指,需要大量连续空间内存的Java对象,最典型的大对象就是那种很长的字符串以及数组,上面代码中的 byte类型的数组就是大对象。大对象堆虚拟机的内存分配来说是一个坏消息,因为新生代的Eden无法存储该大对象的时候,就会直接在老年代中分配对象,如果遇到了一群朝生夕灭的大对象,那是相当糟糕的情况,所以写程序 的时候应尽量避免创建这样的大对象。

经常出现大对象容易导致呢村还有不少空间时就触发垃圾收集以获取足够的连续空间来存放它们

3、长期存活的对象将进入老年代

虚拟机采用了分代收集的算法进行内存管理,在进行内存收集到时候, 就需要给每一个对象定义年龄(age)计数器,对象第一次在Eden区创建后,经历了第一次Minor GC后依然存活,会被移动到Survivor空间中(如果对象过大,Survivor无法存放该对象,会直接进入老年代),并且对象年龄为1,当对象经历了一定次数(默认时15次)的垃圾回收(Minor GC)仍然存活就会进入老年代

  • 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
    官方文档中的介绍:
    在这里插入图片描述
    格式:-XX:MaxTenuringThreshold=10

3、动态年龄判断

为了适应不同程序的内存状况,虚拟机并不是永远的要求对象必须达到MaxTenuringThreshold才能晋升老年代的,如果在Survivor空间中相同年龄所有对象大小的中和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入到老年代中,不用等到MaxTenuringThreshold中要求的年龄。

4、空间分配担保

  1. 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
  2. 如果这个条件成立,那么MinorGC可以确保是安全的,如果不成立,虚拟机会查看HandlePromotionFailure的设置值是否允许担保失败
  3. 如果允许,那么会基础检查老年代最大可用空间是否大于历次晋升到老年代对象的平均大小
  4. 如果大于,就尝试进行一次GC,尽管这次Minnor GC是有风险的,如果小于,或者HandlePromotionFailure设置不允许冒险,那这是也要改进为一次Full GC。

有关冒险的解释:

  • 在新生代中使用复制收集算法,但为了内存利用率,只是用一个Survivor空间来作为轮换备份,当出现大量对象在Minor GC后依然存活的情况下(最极端的情况就是内存回收后新生代对象都存活),就需要老年代进行分配担保,把Survior无法容纳的对象直接进入老年代

  • 这里的前提是老年代本身还有 容纳这些对象的剩余空间,在实际完成回收之前虚拟机时无法明确知道这次GC之后存活对象的数量,所以就将之前每一次回收晋升到老年代的对量平均值作为预估值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

  • 取平均值进行比较其实依然时一种动态概率的手段,如果在Minor GC后实际存活的对象远高于平均值,依然会导致担保失败(Handle Promotion Failure)

如果出现了担保失败,那就只好在失败后重新发起一次Full GC,虽然担保失败时绕的圈子是最大的,但是大部分情况下还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

猜你喜欢

转载自blog.csdn.net/Hicodden/article/details/111085345
今日推荐