JVM内存分配

  1. 优先分配内存到eden区域
    打开idea在配置vm运行参数(-XX:+PrintGCDetails)
    JVM内存分配_第1张图片
    运行下面的代码

    public class Main {
    
        public static void main(String[] args) {
    
            byte[] b1 = new byte[4 * 1024 * 1024];
        }
    }
    

    控制台会打印出如下图vm信息(这里使用的是jdk8,9以上的版本vm信息输出格式好像不是这样的),所以创建对象会优先分配内存到eden区域。
    JVM内存分配_第2张图片

  2. 空间分配担保
           在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。但是jdk1.6之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
    接下来我们通过参数指令限制下堆内存、新生代区域、eden区域的大小
    堆内存限制:-Xms20M -Xmx20M
    新生代区域内存限制:-Xmn10M
    eden区域内存限制:-XX:SurvivorRatio=8(表示Survivor:Eden = 2:8)
    内存分配如下图
    JVM内存分配_第3张图片
    然后运行下列代码(就是故意让eden区域内存不够用,会发生什么事情)

    public class Main {
    
        /**
         * -XX:+PrintGCDetails
         * -Xms20M -Xmx20M -Xmn10M
         * -XX:SurvivorRatio=8
         */
        public static void main(String[] args) {
    
            byte[] b1 = new byte[2 * 1024 * 1024];
    
            byte[] b2 = new byte[2 * 1024 * 1024];
    
            byte[] b3 = new byte[2 * 1024 * 1024];
            
            byte[] b4 = new byte[4 * 1024 * 1024];
        }
    }
    

    控制台输出如下。
    JVM内存分配_第4张图片
    首先会看到有Minor GC

  • Minor GC
    当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
  • Full GC
    调用System.gc时、老年代空间不足、方法去空间不足等都会触发Full GC。

       观察控制台情况所知,当eden区域不够用的时候(新生代内存区域),会向其他区域例如老年代借用内存区域。

  1. 大对象直接分配到老年代
           所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”他们。
           虚拟机提供了一个-XX : PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)。
    XX:PretenureSizeThreshold=3145728(代表超过3M就算大对象)
    由于Parallel Scavenge垃圾收集器不识别PretenureSizeThreshold,所以需要指定Serial收集器-XX:+UseSerialGC
    见下面代码

    public class Main {
    
        /**
         * -XX:+PrintGCDetails
         * -Xms20M -Xmx20M -Xmn10M
         * -XX:SurvivorRatio=8
         * -XX:PretenureSizeThreshold=3145728
         * -XX:+UseSerialGC
         * @param args
         */
        public static void main(String[] args) {
    
            byte[] b1 = new byte[5 * 1024 * 1024];
    
        }
    }
    

    控制台输出如下,可见老年代直接占用了5M内存
    JVM内存分配_第5张图片

  2. 长期存活的对象分配到老年代
           既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
    请看下下面的例子

    public class Main {
    
        /**
         * -XX:+PrintGCDetails
         * -Xms20M -Xmx20M -Xmn10M
         * -XX:SurvivorRatio=8
         * -XX:MaxTenuringThreshold=1 年龄阈值,对象每熬过一次Minor GC,它的age会加1,age达到此值对象就会晋升老年代
         * -XX:+UseSerialGC
         */
        public static void main(String[] args) {
    
            byte[] b1 = new byte[2 * 1024 * 1024];
    
            byte[] b2 = new byte[4 * 1024 * 1024];
    
            b1 = null;
    
            b1 = new byte[2 * 1024 * 1024];
    
        }
    }
    

    输出如下,发生了一次minor gc,是在执行b1 = new byte[2 * 1024 * 1024];的时候,这使得为b1第一次分配的对象gc掉了。
    在gc之前b2的年龄为0,当gc执行的时候,b2没有被gc掉,则age加1,到达设定的年龄阈值,所以就晋升到了老年代。老年代占用空间为4m刚好就是b2的内存大小。
    JVM内存分配_第6张图片

  3. 动态对象年龄判断
           为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
    详细请见 https://yq.aliyun.com/articles/649333

  4. 逃逸分析和栈上分配
    ①. 逃逸分析
    对象的作用于仅在当前方法中有效,称为没有发生逃逸,反之就认为是逃逸对象,简单来说就是方法内的对象,可以被方法外所访问。
    发生逃逸情景举例

  • 方法返回对象

    public class Main {
    
        public Main main;
    
        /**
         * 方法返回Main对象,发生逃逸
         */
        public Main getInstance() {
            return main == null ? new Main() : main;
        }
    }
    
  • 为成员属性赋值

    public class Main {
    
        public Main main;
    
        /**
         * 为成员属性赋值,发生逃逸
         */
        public void setMain() {
            this.main = new Main();
        }
    }
    
  • 引用成员变量的值

    public class Main {
    
        public Main main;
    
        public Main getInstance() {
            return main == null ? new Main() : main;
        }
    
        /**
         * 引用成员变量的值,发生逃逸
         */
        public void useMain() {
            Main main = getInstance();
        }
    }
    

    ②. 栈上分配
    就是把没发生逃逸的对象,在栈分配空间。

  • 对象的作用域仅在当前方法中有效

    public class Main {
    
        public Main main;
    
        /**
         * 对象的作用域仅在当前方法中有效,不发生逃逸
         */
        public void userMain() {
            Main main = new Main();
        }
    }
    
  1. 如果对象发生逃逸,那会分配到堆中。(因为对象发生了逃逸,就代表这个对象可以被外部访问,换句话说,就是可以共享,能共享数据的,无非就是堆或方法区,这里就是堆。)
  2. 如果对象没发生逃逸,那会分配到栈中。(因为对象没发生逃逸,那就代表这个对象不能被外部访问,换句话说,就是不可共享,这里就是栈。)

我们深入思考下,为什么会有逃逸分析,有栈上分配这些东西?(以下引用内容来自逃逸分析和栈上分配)

       当然是为了主体的考虑,主体就是jvm,或者直接说为了GC考虑也不为过。大家想想,GC主要回收的对象是堆和方法区。GC不会对栈、程序计数器这些进行回收的,因为没东西可以回收。
       说回来,如果方法逃逸,那么对象就会分配在堆中,这个时候,GC就要工作了。如果没发生方法逃逸,那么对象就分配在栈中,当方法结束后,资源就自动释放了,GC压根不用操心。所以哈,方法逃逸这东西,主要也是为GC打工的,或者说为GC服务吧!说到这里,可能有人会问,那方法逃逸和性能还是没关系哈?emmm!!!其实有,想深一层,GC不运行的时候,程序的性能肯定会好点,不会占用程序运行的时间。虽然GC清扫垃圾的速度很快,但是当一个程序足够大的时候,对象就自然多了,垃圾也自然多了,这个时候GC就忙了。

你可能感兴趣的:(Java虚拟机)