上文:jvm堆


背景:随着jvm的发展,堆已经不是分配内存的唯一选择了,还有栈上分配、标量替换优化技术。

逃逸分析是什么?

    编程语言的编译优化原理中,分析指针动态范围的方法称之为逃逸分析。可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。也就是说通过逃逸分析可以判断对象的引用和使用范围从而决定是否要将这个对象分配到堆上面。

注意:逃逸分析不是直接的优化手段,而是分码分析手段。

什么条件下会触发逃逸分析?

Java的逃逸分析只发在JIT的即时编译中,因为在启动前已经通过各种条件判断出来是否满足逃逸,通过上面的流程图也可以得知对象分配不一定在堆上,所以可知满足逃逸的条件如下,只要满足以下任何一种都会判断为逃逸。

        一、对象被赋值给堆中对象的字段和类的静态变量。

        二、对象被传进了不确定的代码中去运行。

对象逃逸的范围有:全局逃逸、参数逃逸、没有逃逸;

逃逸分析案例

相关配置

开启逃逸分析(JDK8中,逃逸分析默认开启。)
-XX:+DoEscapeAnalysis
关闭逃逸分析
-XX:-DoEscapeAnalysis
查看逃逸分析结果
-XX:+PrintEscapeAnalysis
package com.escape;

import com.Student;

/**
 * @author: csh
 * @Date: 2021/4/21 11:01
 * @Description:线程逃逸
 */
public class ThreadEscape {

    public static Integer sum=0;


    
    /**
     * 详细日志
     * -XX:+PrintGCDetails
     * 简单日志
     * -XX:+PrintGC
     *
     * 功能描述: -XX:+PrintGC -Xms5M -Xmn5M -XX:+DoEscapeAnalysis
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 13:48
     */
    public static void main(String[] args) throws InterruptedException {
        //-XX:-DoEscapeAnalysis 未开启锁消除 234毫秒 //-XX:+DoEscapeAnalysis 开启则128毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// escape("a", "b");
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");

        //-XX:-DoEscapeAnalysis 313毫秒 -XX:+DoEscapeAnalysis 开启:273毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().noEscape("a", "b");
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");

        //-XX:-DoEscapeAnalysis 关闭:53毫秒 -XX:+DoEscapeAnalysis 开启:43毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().setSum(i);
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");

        //-XX:-DoEscapeAnalysis 关闭:145毫秒 -XX:+DoEscapeAnalysis 开启:8毫秒
// long start = System.currentTimeMillis();
// for(int i =0;i<5_000_000;i++){
// new ThreadEscape().newLockNoEscape();
// }
// long end = System.currentTimeMillis();
// System.out.println("耗时"+(end-start)+"毫秒");

        long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);
    }


    /**
     *
     * 功能描述: 全局变量赋值发生逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 14:23
     */
    public void setSum(Integer number){
        sum = number;
    }

    /**
     *
     * 功能描述: 线程逃逸,因为该StringBuffer 会将结果返回,可能被其他地方所引用
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 11:03
     */
    public static StringBuffer escape(String s1, String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
        return stringBuffer;
    }

    /**
     *
     * 功能描述: 未发生逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 11:05
     */
    public  void noEscape(String s1,String s2){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(s1);
        stringBuffer.append(s2);
    }

    /**
     *
     * 功能描述: 创建线程可见,但对象无法逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/21 14:30
     */
    public void newLockNoEscape(){
        //创建线程可见,但对象无逃逸
        synchronized (new Object()){

        }
        //创建线程可见,但对象无逃逸
        Object noEscape = new Object();
    }

    /**
     *
     * 功能描述: 方法逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/22 11:55
     */
    public static Object newObject(){
        return new Object();
    }

    /**
     *
     * 功能描述: 没有逃逸
     *
     * @param:
     * @return:
     * @auther: csh
     * @date: 2021/4/22 11:55
     */
    public static void newObject2(){
        new Object();
    }


}

测试如下:

-XX:-DoEscapeAnalysis -XX:+PrintGC

long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);

结果:39毫秒,一次gc 并且有一百多万的垃圾回收。

[GC (Allocation Failure)  65536K->880K(251392K), 0.0013300 secs]
耗时39毫秒

-XX:+DoEscapeAnalysis -XX:+PrintGC

long start = System.currentTimeMillis();
        for(int i=0;i<5000000;i++){
            newObject();
        }
// for(int i =0;i<5000000;i++){
// newObject2();
// }
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end-start)+"毫秒");
        Thread.sleep(100000);

结果:只有4毫秒,没有gc,提高了快10倍效率,并且堆中只有十几万。逃逸了

耗时4毫秒

    可以发现一个逃逸和没逃逸的问题,只要是对象有被方法外部或者全局引用到那肯定会存在逃逸。

当对象没有发生逃逸的时候,虚拟机会对其进行优化如下:

    同步消除(Synchronization Elimination)

        假如对象是在方法中,并且存在同步锁,jvm会消除该锁,以此来提高性能。(jdk8开始默认开启锁消除)通过-XX:+EliminateLocks可以开启同步消除。

    标量替换(Scalar Replacement)

        若一个数据已经分解到不能再小了,这种称为标量。如果可以再继续分解则称为聚合量。那么标量替换就是将一个java对象拆散,根据程序的访问情况,将其用到成员变量恢复为原始类型来访问,称为标量替换。

    -XX:+EliminateAllocations可以开启标量替换

    -XX:+PrintEliminateAllocations查看标量替换情况

    栈上分配 (Stack Allocations)

        如果一个对象没有发生逃逸,那这个对象会被标量替换,这个对象的成员信息会被分配到线程栈,而不会在堆上分配,这样对象会随着栈帧的销毁而销毁,从而减少GC的压力,节约空间,提升性能。

最后

    jdk8开始默认是开启逃逸分析,经过非常多的验证发现,只要开发性能总体来说比没有之前快,有些快了几十上百倍,有些则几倍。也就是说间接证明了不一定发生了逃逸分析才会加速效率,jvm还做了其他大量的优化,只要开启逃逸分析默认就OK了。

参考文章:

    https://zhuanlan.zhihu.com/p/59215831    https://zh.wikipedia.org/wiki/%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90

    https://dl.acm.org/doi/10.1145/320385.320386    https://blog.csdn.net/sky15256567734/article/details/106786870

    http://staff.ustc.edu.cn/~yuzhang/papers/cncc07.pdf   

系列文章:

jvm相关知识

java发展史及虚拟机历史

对象的内存是如何布局的?

jvm的类加载器(classloader)及类的加载过程

java模块化系统

深入栈帧

深入方法区

jvm堆

...

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐