25

【拥抱大厂系列】百度面试官问过的 “JVM内存分配与回收策略原理”,我用这篇文章搞定了

 4 years ago
source link: https://segmentfault.com/a/1190000022503399
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了3个月总结的一线大厂Java面试总结,本人已拿腾讯等大厂offer。

在前面的一篇文章 深入理解Java虚拟机-如何利用VisualVM进行性能分析 中讲到了一些关于JVM调优的知识,但是,其实,还是有一些问题没有非常清楚的可以回答的,这里先给出几个问题,然后,我们再展开这篇文章需要讲解的知识。

  • 我们生成的对象最开始在哪分配?Eden?Survivor?还是老年代呢?
  • 进入到老年代需要满足什么条件呢?

接下来,我们就带着这两个问题展开全文。

1 对象优先在哪分配

其实,通过前面几篇文章的讲解,这个问题其实已经见怪不怪了,在大多数的情况下,对象都是在新生代 Eden区 分配的,在前面的文章我们提到,在Eden区中如果内存不够分配的话,就会进行一次 Minor GC 。同时,我们还知道年轻代中默认下 Eden:Survivor0:Survivor2 = 8:1:1 ,同时,还能通过参数 -XX:SurvivorRatio 来设置这个比例(关于这些参数的分析都可以查看这篇文章: 深入理解Java虚拟机-常用vm参数分析 )。

下面我们通过一个例子来分析是不是这样的。

1.1 实例

给定JVM参数:-Xms40M -Xmx40M -Xmn10M -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=4

前面三个参数设置Java堆的大小为40M,新生代为10M,紧跟着后面两个是用于输入GC信息。更多参数可以查看这篇文章: 深入理解Java虚拟机-常用vm参数分析

/**
 * @ClassName Test_01
 * @Description 参数:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8
 * @Author 欧阳思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[5 * M];
        alloc2 = new byte[5 * M];
        alloc3 = new byte[5 * M];
        alloc4 = new byte[10 * M];

    }

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

}

输入结果:

Y3uqY3M.png!web

分析

  • eden:from:to=8:1:1,这个因为前面设置了参数 -XX:SurvivorRatio=8
  • 新生代分配了20M的内存,所以前面三个 byte 数组可以分配,但是,分配第四个的时候,空间不够,所以,需要进行一次 Minor GC ,GC之后,新生代从 12534K 变为 598K
  • 前面在新生代分配的内存 Minor GC 之后,进入到了 Survivor ,但是,Survivor不够分配,所以进入到了 老年代 ,老年代已用内存达到了 50%

1.2 回答问题

所以,经过上面的例子我们发现,对象一般优先在新生代分配的,如果新生代内存不够,就进行Minor GC回收内存。

2 进入到老年代需要满足什么条件

先给出答案,分为几点。

  • 条件① :大对象直接进入到老年代
  • 条件② :长期存活的对象可以进入到老年代
  • 条件③ :如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入到老年代

2.1 分析条件①

  • 哪些属于大对象呢?

一般来说大对象指的是 很长的字符串及数组 ,或者 静态对象

  • 那么需要满足多大才是大对象呢?

这个虚拟机提供了一个参数 -XX:PretenureSizeThreshold=n ,只需要大于这个参数所设置的值,就可以直接进入到老年代。

step1:解决了这两个问题,首先,我们不设置上面的参数的例子,将对象的内存大于Eden的大小看看情况。

/**
 * @ClassName Test_01
 * @Description 参数:-Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * @Author 欧阳思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
//        alloc1 = new byte[5 * M];
//        alloc2 = new byte[5 * M];
//        alloc3 = new byte[5 * M];
        alloc4 = new byte[22 * M];

    }

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

}

r2UVfui.png!web

我们发现分配失败,Java堆溢出,因为超过了最大值。

step2:下面我们看一个例子:设置 -XX:PretenureSizeThreshold=104,857,600 ,这个单位是B字节(Byte/bait),所以这里是 100M

/**
 * @ClassName Test_01
 * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=104,857,600
 * @Author 欧阳思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
//        alloc1 = new byte[5 * M];
//        alloc2 = new byte[5 * M];
//        alloc3 = new byte[5 * M];
        alloc4 = new byte[500 * M];

    }

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

}

bQJNnyn.png!web

发现新生代没有分配,直接在老年代分配。

注意: 参数 PretenureSizeThreshold 只对 SerialParNew 两款收集器有效。

2.2 分析条件②

进入老年代规则: 这里需要知道虚拟机对每个对象有个对象年龄计数器,如果对象在Eden出生经过第一次Minor GC后任然存活,并且能够被Survivor容纳,将被移动到Survivor空间中,并且年龄设置为1。接下来,对象在Survivor中每次经过一次Minor GC,年龄就增加1,默认当年龄达到15,就会进入到老年代。

晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。

在下面的实例中,我们设置 -XX:MaxTenuringThreshold=1

/**
 * @ClassName Test_01
 * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
 * @Author 欧阳思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[300 * M];
        alloc2 = new byte[300 * M];
        alloc3 = new byte[300 * M];
        alloc4 = new byte[500 * M];

    }

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

}

bYvmYvr.png!web

从结果可以看出,from和to都没有占用内存,而老年代则占用了很多内存。

2.3 分析条件③

条件③是:如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入到老年代,而不需要等到参数 -XX:MaxTenuringThreshold 设置的年龄。

实例分析

/**
 * @ClassName Test_01
 * @Description 参数:-Xms2048M -Xmx2048M -Xmn1024M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
 * @Author 欧阳思海
 * @Date 2019/12/3 16:00
 * @Version 1.0
 **/
public class Test_01 {

    private static final int M = 1024 * 1024;

    public static void test() {
        byte[] alloc1, alloc2, alloc3, alloc4;
        alloc1 = new byte[100 * M];
        alloc2 = new byte[100 * M];
        //分配alloc3之前,空间不够,所以minor GC,接着分配alloc3=900M大于Survivor空间一半,直接到老年代。
        alloc3 = new byte[900 * M];
        
//        alloc4 = new byte[500 * M];

    }

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

}

输入结果:

NZfEbyB.png!web

分配alloc3之前,空间不够,所以minor GC,接着分配alloc3=900M大于Survivor空间一半,直接到老年代。从而发现,survivor占用0,而老年代占用900M。

3 总结

这篇文章主要讲解了JVM内存分配与回收策略的原理,回答了下面的这两个问题。

  • 我们生成的对象最开始在哪分配?Eden?Survivor?还是老年代呢?
  • 进入到老年代需要满足什么条件呢?

另外,我花了3个月时间把Java学习和面试的总结整理成了一本电子书!目录如下

jIzaeqI.png!web

现在免费分享大家,在我的公众号 好好学java 回复 Java面试 即可获取。

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

1、老铁们,关注我的原创微信公众号「 好好学java 」,专注于Java、数据结构和算法、微服务、中间件等技术分享,保证你看完有所收获。

2、给俺点个赞呗,可以让更多的人看到这篇文章,顺便激励下我继续写作,嘻嘻。

点赞是对我最大的鼓励

↓↓↓↓↓↓


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK