java虚拟机笔记 逃逸分析

逃逸分析
  • 背景:在Java虚拟机中,对象都是在java堆中分配内存的,这是一个普遍的常识,但是有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么该对象就可能被优化成栈上分配。
  • 逃逸分析并不是直接的优化手段,而是一个代码分析;为其它优化手段如栈上分配、标量替换和同步消除等提供依据
  • 为什么要将堆上的对象分配到栈上(逃逸分析技术的目的)
    • 减少堆分配的压力,因为堆是共享区域,堆上分配需要进行同步操作
    • 主要是为了减少gc次数,提高gc效率。因为gc只会清理堆区和方法区,而不用清理虚拟机栈,将对象分配在栈上,随着方法的出栈,对象被释放,也不用考虑资源释放的问题
逃逸分析的原理(怎么分析对象有没有逃逸)
  • 逃逸分析的基本行为就是分析对象动态作用域
    • 当一个对象在方法中被定义后,且对象只在方法内部使用,则认为没有发生逃逸(对象可以分配在栈上)
    • 当一个对象在方法中被定义后,如果它(指的是对象,而不是变量)可以被外部方法所引用,则认为发送了逃逸。例如一个对象作为调用参数传到其他方法中则认为发生了逃逸
public class EscapeAnalysis {
    private EscapeAnalysis instance;

    public EscapeAnalysis getInstance(){
        //instance对象被作为方法返回值,可能在其他地方被引用,所以发生了逃逸
        return  instance==null?new EscapeAnalysis():instance;
    }
    public void setFiled(){
        //为成员属性赋值,发生了逃逸,因为该成员属性也有可能在其他地方被引用
        this.instance = new EscapeAnalysis();
    }
    public void methodA(){
        //对象的作用域仅仅在当前方法有效,所以没有发生逃逸,该对象可以(不一定)在栈上分配
        EscapeAnalysis e = new EscapeAnalysis();
    }
    public void methodB(){
        //发生了逃逸,虽然变量e的作用域仅仅在当前方法有效,但是变量e所引用的对象可能在其他地方有引用
        //所以进行逃逸分析时是对被引用的对象进行分析而不是变量
        EscapeAnalysis e = getInstance();
    }
}
参数相关
  • 在jdk6u23及其之后的版本,HotSpot默认开启了逃逸分析
  • 可以通过 -XX:+DoEscapeAnalysis显式开启逃逸分析
  • 可以通过 -XX:+PrintEscapeAnalysis查看逃逸分析的筛选结果
代码优化

基于逃逸分析,即时编译器可以对代码进行以下优化:

  • 栈上分配:
    • JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就有可能被优化成栈上分配.方法执行结束,栈空间被回收,局部变量对象也被回收,无需进行垃圾回收
    • 注意,其实栈的空间容量也比较小,所以只有一些占用内存空间较小的对象才可以在栈上分配,如果占用内存较大还是会在堆上进行分配
  • 同步省略
    • 如果一个对象发现只能从一个线程访问到(即没有发生逃逸),对这个对象的操作可以不考虑同步,可以避免同步带来性能损耗,提升性能
    public void methodC(){
        EscapeAnalysis escape = new EscapeAnalysis();
        synchronized (escape){
            System.out.println("hhhh");
        }
        //因为escape对象没有发生逃逸,也不会被其他线程访问到(方法局部变量),所以运行时被编译器优化如下
        //这个也被称为锁消除
        //EscapeAnalysis escape = new EscapeAnalysis();
        //System.out.println("hhhh");
        
    }
  • 标量替换
    • 标量:指一个无法分解成更小数据的数据。java里的原始数据类型就是标量
    • 聚合量:还可以分解的数据,比如java里的对象
    • JIT编译器如果经过逃逸分析发现一个对象不会被外界访问到(没有逃逸),那么经过JIT优化,就会把这个对象拆解成若干个标量来代替,这个过程就是标量替换
    • 标量替换默认开启,可以通过 -XX:+EliminateAllocations 参数显式开始
// jdk1.8 环境下
//-Xms2g -Xmx2g -XX:+PrintGC 初始堆内存和最大堆内存都设置为2G内存,并打印gc的情况下(逃逸分析默认开启)
//分别在开启标量替换和关闭标量替换查看Point对象分配情况
//-XX:-EliminateAllocations 关闭标量替换
//-XX:+EliminateAllocations 显式开启标量替换(默认开启)
public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public static void main(String[] args) throws InterruptedException{
        for (int i = 0; i < 1000000; i++) {
            //执行100万次alloc方法,生成100万个Point对象
            alloc();
        }
        Thread.sleep(100000);
    }
    public static void alloc(){
        Point point = new Point(1,2);
        //经过标量替换,代码被优化成
        //int x=1;
        //int y=2;
    }

}
  • 在开启逃逸分析、关闭标量替换的环境下运行,通过java visual VM工具查看堆内存,如下:可以看到100w个Point对象都分配在堆上了
    在这里插入图片描述

  • 在开启逃逸分析、开启标量替换的环境下运行,如下:只有8w多个Point对象分配到堆上,剩下通过标量替换的方式优化了。
    在这里插入图片描述
    那为什么开启了标量替换,还是有8w多个对象分配到堆中?

    • 这和即时编译器JIT有关优化策略有关:即时编译器只在代码段一段时间内执行足够次数时才会对代码段进行优化(即利用JIT编译器重新编译代码段),在执行过程中不断收集各种数据,作为优化的决策,所以在优化完成之前,例子中的Point对象还是在堆上进行分配
扩展:关于即时编译器的两个版本
  • Hotspot可以以客户端(client)模式或者服务端(Server)端模式启动
  • HotSpot的Client模式是为了减少启动时间而优化的,且运行过程中采用的即时编译器是Client Compiler(也叫C1编译器),特点是只是对字节码指令进行简单且可靠的的优化,优化方式比较保守,但编译的时间耗时短
  • HotSpot的Server模式是为了服务器环境中最大化程序执行速度而设计,运行过程中采用的即时编译器是Server Compiler(也叫C2编译器),特点是编译字节码指令时需要耗费较长的时间,优化方式比较激进,但优化后的字节码指令执行效率更高。逃逸分析是C2编译器优化代码的基础。根据java8的官方文档,64位版本的jdk仅支持Server模式启动。
  • jdk1.8后,Server模式启动的HotSpot默认开启了分层编译的(-XX:+TieredCompilation)配置,所谓的分层编译,在系统执行初期,执行频率比较高的代码先被c1编译器编译,以便尽快进入编译执行,然后随着时间的推移,执行频率较高的代码再被c2编译器编译,以达到最高的性能。
结论
  • 直到现在逃逸分析技术还不是非常成熟,根本原因是无法保证逃逸分析的性能消耗一定高于它的消耗,虽然经过逃逸分析可以做标量替换、栈上分配和锁消除,但是逃逸分析自身也是需要进行一系列复杂的分析,这其实也是一个相对耗时的过程
  • 虽然这项技术不是十分成熟,但他也是即时编译器优化技术中一个十分重要的手段
  • 虽然在栈上分配对象在理论上是可行的,但是现在HotSpot虚拟机并没有那么做(但是实现了标量替换),HotSpot虚拟机里对象实例都是创建在堆上的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值