JVM对象内存分配流程

对象内存分配流程图

在这里插入图片描述

对象栈内分配

        通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有引用的时候,需要依靠GC来进行回收内存,如果对象数量较多的时候,会给GC带来较大的压力,也间接影响了应用的性能.为了减少临时对象在堆内存分配的数量,JVM通过逃逸分析确定该对象会不会被外部访问.如果不会逃逸可以将该对象在栈内分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力.
**逃逸分析:**就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用.例如:作为参数传递到其他的方法中.

public class StackAlloc {
    
    

    public User test1(){
    
    
        User user = new User();
        user.setUserId(1);
        user.setUserName("fanqiechaodan");
        // 持久化到DB
        return user;
    }

    public void test2(){
    
    
        User user = new User();
        user.setUserId(1);
        user.setUserName("fanqiechaodan");
        // 持久化到DB
    }
}

        很显然test1方法中的user被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结束后就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内存一起被回收掉.
        JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配到栈上 (栈内分配),JDK7之后默认开启逃逸分析, 如果需要关闭使用参数 (-XX:-DoEscapeAnalysis)
标量替换: 通过逃逸分析确定该对象不会被外部访问,并且对象可以进一步分解时,JVM不会创建该对象,而是将该对象的成员变量分解若干个被这个方法使用的成员变量代替,这些代替的成员变量在栈帧或者寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配.开启标量替换参数(-XX:+EliminateAllocations), JDK默认开启标量替换
标量与聚合量: 标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量就称之为聚合量,而在JAVA中对象就是可以被进一步分解的聚合量.

栈内分配示例:

/**
 * @author fanqiechaodan
 * @Classname AllotOnStack
 * @Description 栈上分配,标量替换
 * 代码调用了1亿次test(),如果是分配到堆上,15m是肯定不够用的,必然会触发GC。
 *
 * 使用如下参数不会发生GC
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * 使用如下参数都会发生大量GC
 * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
 * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
 * @Date 2021/11/16 21:17
 */
public class AllotOnStack {
    
    

    public static void main(String[] args) {
    
    
        for (int i = 0; i < 100000000; i++) {
    
    
            test();
        }
    }

    private static void test() {
    
    
        User user = new User();
        user.setUserId(1);
        user.setUserName("fanqiechaodan");
    }
}

结论:栈内分配依赖于标量替换和逃逸分析;

对象在Eden区分配

        大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够的空间进行分配时,JVM将发起一次minorGC

  • minorGC: 指发生在新生代的垃圾收集动作,minorGC非常频繁,回收速度一般也比较快.
  • FullGC: 一般会回收老年代,年轻代,方法区的垃圾,FullGC的速度一般会比minorGC的慢10倍以上.

Eden与Survivor区默认8:1:1
        大量的对象被分配在Eden区,Eden区满了以后会触发minorGC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到空闲的Survivor区,下次Eden区满了后又会触发minorGC,把Eden区和非空闲的Survivor区垃圾对象回收,把剩余的存活的对象一次性挪到另一块空闲的Survivor区,因为新生代的对象都是朝生夕死的,存活的时间很短,所以JVM默认的8:1:1的比例还是很合适的,让Eden区尽量的大,Survivor区够用即可
        JVM默认有这个参数 -XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1的比例自动变化,如果不想这个比例有变化可以设置参数 -XX:-UseAdaptiveSizePolicy

大对象直接进入老年代

        大对象就是需要大量连续内存空间的对象,例如:字符串,数组.JVM参数 -XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在SerialParNew两个收集器下有效.例如:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC,如果对象大小超过了1000000字节,就会直接进入老年代;
大对象直接进入老年代的优点: 避免大对象分配内存时的复制操作而降低效率.

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

        既然JVM采用了分代收集的思想来管理内存,那么内存在回收时就必须能识别哪些对象应放在新生代,那些对象应放在老年代中,为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器.如果对象在Eden出生并经过第一次minorGC后仍然能够存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor每经历一次minorGC,年龄就+1,当它的年龄增加到一定的成都(默认15岁,CMS收集器默认6岁,不同的收集器略微会有点不同),就会晋升到老年代中,对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold来设置.

对象动态年龄判断

        当前非空的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,

  • 例如: 非空的Survivor区域里现在有一批对象,年林1+年龄2+年龄n+年龄等多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄大于等于n的对象都放入老年代

        这个机制其实是希望那些可能是长期存活的对象,尽早的进入老年代.对象动态年龄机制一般是在minorGC之后触发的.

老年代空间分配担保机制

  • 年轻代每次minor GC之前JVM都会计算下老年代剩余可用空间
  • 如果这个可用空间小于年轻代现有的所有对象大小之和 (包括垃圾对象)
  • 就会看是否设置 -XX:-HandlePromotionFailure(jdk1.8默认设置)
  • 如果有设置,就会查看老年代的可用内存大小,是否大于之前每一次minor GC后进入老年代的对象的平均大小
  • 如果没有设置或者老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发一次Full GC,对老年代和年轻代一起回收一次垃圾,如果回收还是没有足够空间释放新的对象就会发生OOM
  • 如果有设置并且老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发minor GC,当然如果minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有空间放置minor GC之后的存活对象,则也会发生OOM
    在这里插入图片描述

对象内存回收

        堆中几乎存放着所有的对象实例,对垃圾回收前的第一步就是要判断那些对象已经死亡;即不能再被任何途径使用的对象

引用计数法

        对每个对象的引用进行计数,每当有一个地方引用它时计数器+1,引用失效则-1.引用计数放到对象头中,大于0的对象被认为是存活对象.

public class ReferenceCounting {
    
    

    Object eg = null;

    public static void main(String[] args) {
    
    
        ReferenceCounting referenceCounting1 = new ReferenceCounting();
        ReferenceCounting referenceCounting2 = new ReferenceCounting();
        referenceCounting1.eg = referenceCounting2;
        referenceCounting2.eg = referenceCounting1;
        referenceCounting1 = null;
        referenceCounting2 = null;
    }
}

        如上面代码所示:除了对象referenceCounting1和referenceCounting2相互引用着对方以外,这两个对象之间再无任何引用,但是他们因为互相引用对方,导致它们的引用计数器都不为0;出现相互循环引用的问题,会导致GC回收器无法进行回收,引用计数法是可以解决循环引用问题的,主要是通过Recycler算法进行解决,但是再多线程环境下,引用计数变更也需要进行昂贵的同步操作,性能较低.目前主流的虚拟机并没有选择这个算法来管理内存

可达性分析

        从GC Root开始进行对象搜索,可以被搜索到的对象即为**可达对象,**此时还不足以判断对象是否存活/死亡,需要经过多次的标记才能更加准确的确定,非可达对象便可以作为垃圾被回收掉. 目前Java中主流的虚拟机均采用此算法.
        GC Root的根节点可以是线程栈的本地变量,静态变量,本地方法栈的变量等等.

在这里插入图片描述

Guess you like

Origin blog.csdn.net/qq_43135259/article/details/121359446