HotSpot 内存分配的主要规则

2020-11-20

分配与回收,是 Java 虚拟机自动管理内存的两个部分。

之前提的垃圾内存回收的三种算法与常见的垃圾收集器,这些都是内存的回收,那 JVM 是如何分配内存呢?

Java 虚拟机规范并没有规定对象的创建和存储的细节,每款收集器都有各自的实现。

 

我本地环境是:

JDK 1.8、64 位、win7、HotSpot 默认 JVM 配置 

Parallel Scavenge + Parallel Old 组合,会加参数改收集器

 

1、新对象,一般会优先被分配到新生代 Eden 区,当 Eden 区没有足够空间虚拟机将发起 Minor GC

代码:

package constxiong.jvm.gc;

/**
 * 测试分配对象到 Eden 区
 */
public class AllocateToEden {

    public static void main(String[] args) {
    }

}

参数:  

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails

GC 日志:

说明:

-Xms20M 初试堆内存大小 20M
-Xmx20M 最大堆内存大小 20M
-Xmn10M 新生内存大小 10M
-XX:SurvivorRatio=8 新生代中 Eden:Survivor:Survivor=8:1:1,Eden 占 80%
-XX:+PrintGCDetails 打印 GC 日志详情
这样配,

堆的大小为 20M
新生代:Eden 就 8M = 8192K,两个 Survivor(from、to) 分别为 1M = 1024K
老年代:10M = 10240K


上面代码启动并未分配对象,Eden 消耗了 2314K,元空间 Metaspace 消耗了 3458K

 

修改代码在 main 方法分配一个 2M 的字节数组

代码:

package constxiong.jvm.gc;

/**
 * 测试分配对象到 Eden 区
 */
public class AllocateToEden {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024];//2M 数组
    }

}

GC 日志:

说明:

2M 数组对象,别分配到了新生代的 Eden 区,未分配对象前 Eden 消耗 2314K,加上 2048K,等于 4362K

 

 

2、占用连续空间的大对象(超长字符串、数组),可以直接分配到老年代,避免在新生代的两个 Survivor 区来回复制大对象

修改代码,再分配一个大对象 8M 数组:

package constxiong.jvm.gc;

/**
 * 测试分配大对象到老年代
 */
public class AllocateToOldGeneration {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024]; //2M 数组
        byte[] array2 = new byte[8 * 1024 * 1024]; //8M 数组
    }

}

参数未变:  

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails

GC 日志:

说明:

新生代已经不够分配 8M 的数组对象,所以直接分配到老年代

 

-XX:PretenureSizeThreshold 可以指定当对象大小超过这个阀值时,会被分配到老年代;但 Parallel Scavenge 并不支持,所以添加参数 -XX:PretenureSizeThreshold=3145728,指定垃圾收集器为 Serial ,同时修改代码,把 8M 的数组改为 3M

代码:

package constxiong.jvm.gc;

/**
 * 测试分配对象到老年代
 */
public class AllocateToOldGeneration {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024]; //2M 数组
        byte[] array2 = new byte[3 * 1024 * 1024]; //3M 数组
    }

}

参数:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728

GC 日志:

 

 

3、存活超过一定年龄的对象会被移到老年代

对象熬过一次 Minor GC 年龄就增加,默认配置年龄超过 15 之后,对象会被移到老年代,可以通过 -XX:MaxTenuringThreshold 进行配置该阀值

代码:

package constxiong.jvm.gc;

/**
 * 测试移动超过一定年龄的对象到老年代
 */
public class AllocateToOldGeneration {

    public static void main(String[] args) {
        byte[] array1 = new byte[1024 * 1024 / 10]; //0.1M
        byte[] array2 = new byte[4 * 1024 * 1024]; //4M
        array2 = null;
        array2 = new byte[4 * 1024 * 1024]; //4M
    }

}

参数:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=3 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=1

GC 日志:

说明:

-XX:SurvivorRatio=3,eden=6M、from survivor=2M、to survivor=2M

 

byte[] array2 = new byte[4 * 1024 * 1024]; 这行代码执行的时候触发第一次 GC,此时 array1 和部分其他启动时创建的对象继续保留在新生代且年龄加 1


最后一行代码,array2 = new byte[4 * 1024 * 1024]; 代码执行,触发第二次 GC,由于参数中配置了 -XX:MaxTenuringThreshold=1,年龄大于等于 1 的对象都移动到老年代,此次 GC 后新生代为 0

 

修改参数,把 -XX:MaxTenuringThreshold=1 改为 15

代码不变,参数:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=3 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15

GC 日志:

说明:

修改 -XX:MaxTenuringThreshold=15,代码未变,array1 对象仍然保留在新生代,此时新生代不为 0


这条规则需要同时结合下一条:相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代。如果本例中的参数 -XX:SurvivorRatio=3 仍然等于 8 的话,第二次 GC 之后新生代仍然为 0,因为相同年龄的对象和大于 survivor 区 1M 的一半了

 

4、相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代

代码:

package constxiong.jvm.gc;

/**
 * 测试相同年龄所有对象占用内存之和,大于 Survivor 区空间一半时,大于等于该年龄的对象会被移到老年代
 */
public class AllocateToOldGeneration {

    public static void main(String[] args) {
        byte[] array1 = new byte[1024 * 1024 / 4]; //0.25M
        byte[] array2 = new byte[4 * 1024 * 1024]; //4M
        byte[] array3 = new byte[4 * 1024 * 1024]; //4M,触发第一次 GC
        array3 = null;
        array3 = new byte[4 * 1024 * 1024]; //4M,触发第二次 GC,把所有年轻代的对象移动到老年代
    }

}

参数:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC -XX:MaxTenuringThreshold=15

GC 日志:

说明:

byte[] array3 = new byte[4 * 1024 * 1024]; 这行代码执行的时候触发第一次 GC,此时 array2 被移到老年代,array1 和部分其他启动时创建的对象继续保留在新生代且年龄加 1
最后一行代码,array3 = new byte[4 * 1024 * 1024]; 代码执行,触发第二次 GC,参数中配置了 XX:MaxTenuringThreshold=15,但相同年龄的对象占用内存大于 Survivor 区的一半,所有都移动到老年代,此次 GC 后新生代为 0

 

ConstXiong 备案号:苏ICP备16009629号-3