聊聊JVM逃逸分析

一、JVM的运行模式

1、解释模式(Interpreted Mode)

只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码 编译模式(Compiled Mode)

2、只使用编译器(-Xcomp JVM使用编译模式)

先将所有JVM字节码一次编译为机器码,然 后一次性执行所有机器码

3、混合模式(Mixed Mode)

解释模式和只使用编译器的模式都有明显的缺陷,哪能不能将两种模式整合一下呢?这就是混合模式(JVM默认使用)。

依然使用解释模式执行代码,但是对于一些 "热点" 代码采用编译模式执行,JVM一般采用混合模式执行代码

解释模式启动快,对于只需要执行部分代码,并且大多数代码只会执行一次的情况比较适合;

编译模式启动慢,但是后期执行速度快,而且比较占用内存,因为机器码的数量至少是JVM字节码的十倍以上,这种模式适合代码可能会被反复执行的场景;

混合模式是JVM默认采 用的执行代码方式,一开始还是解释执行,但是对于少部分 “热点 ”代码会采用编译模式执行,这些热点代码对应的机器码会被缓存起 来,下次再执行无需再编译,这就是我们常见的JIT(Just In Time Compiler)即时编译技术。

在即时编译过程中JVM可能会对我们的代码最一些优化,比如对象逃逸分析等!!!

二、JVM内存逃逸分析

JVM的内存分配主要在是运行时数据区(Runtime Data Areas),而运行时数据区又分为了:方法区,堆区,PC寄存器,Java虚拟机栈(就是栈区,官方文档还是叫Java虚拟机栈),本地方法区,而内存逃逸主要是对象的动态作用域的改变而引起的,故而内存逃逸的分析就是分析对象的动态作用域。

1、方法逃逸

什么是方法逃逸呢?举个例子:在一个方法内部new一个对象,最后将这个对象返回给方法调用处。此时该对象的作用域就不是在方法内了,而是传递到了方法外部,这就叫做方法逃逸。

代码如下:

public void test(){
   // 获取到 getCustomer 方法内部创建的 customer
   final Customer customer = getCustomer();
   System.out.println(customer.getName());
}

public Customer getCustomer() {
   final Customer customer = new Customer();
   // 将该对相关传递到方法外部
   return customer;
}

2、线程逃逸

以上面那个方法逃逸的例子,如果这个创建对象的线程是线程A,但是创建后赋值给了外部变量X,恰好这个外部变量又被线程B所引用,所以此时就造成了线程逃逸。

代码如下:

public void test02() throws ExecutionException, InterruptedException {
   // 创建一个callable任务(便于获取返回值)
   final FutureTask<Customer> task = new FutureTask<>((Callable<Customer>) () -> {
      // ①、子线程创建对象
      final Customer customer = new Customer();
      System.out.println("子线程执行,并创建Customer对象: " + customer);
      // ②、将子线程方法内部创建的对象返回给主线程
      return customer;
   });
   // 执行子线程任务
   new Thread(task).start();
   // ③、主线程阻塞获取到子线程创建的对象
   final Customer result = task.get();
  	// 主线程修改从子线程获取的对象属性
		result.setName("wenpan");
   System.out.println("主线程获取到子线程的返回值:" + result);
}

总的来说就是一个对象的指针被多个方法或者线程引用时,我们就称这个对象的指针发生了逃逸。

3、优化

如果能够确定一个对象不会发生逃逸(不会逃逸到方法或线程外),则可以对这个对象的分配进行优化!

①、栈上分配

说起对象,那你第一个想到的是在堆空间上进行内存分配,GC在堆空间上筛选可回收的对象,回收对象,整理内存都需要浪费时间,若能通过逃逸分析确定某些对象是一定不会逃逸出方法之外的,就可以直接让这个对象在栈上分配内存,该对象随方法的执行结束栈帧出栈而销毁,减轻了GC的压力。

②、同步消除

线程同步本身比较耗时,若确定了一个变量不会逃逸出线程,无法被其他线程访问到,那这个变量的读写就不会存在竞争,这个变量的同步措施就可以清除掉。

public void test03() {
  Integer i = 0;
  // 这里经过分析,发现i不会逃逸出线程,不会存在竞争,所以这里的同步锁会被消除
  synchronized (this) {
    i++;
  }
}

③、标量替换

标量:Java中的原始数据类型(int,char,long等)都不能再进一步分解,他们就可以称为标量。

聚合量:若一个数据可以继续分解,那就称之为聚合量,而对象就是典型的聚合量。

④、总结

若逃逸分析证明一个对象不会逃逸出方法,不会被外部访问,并且这个对象是可以被分解的,那程序在真正执行的时候可能不创建这个对象,而是直接创建这个对象分解后的标量来代替。这样就无需在对对象分配空间了,只在栈上为分解出的变量分配内存即可。

4、逃逸相关JVM参数

①、说明

逃逸分析是比较耗时的,所以性能未必提升很多,因为其耗时性,采用的算法都是不那么准确但是时间压力相对较小的算法来完成的,这就可能导致效果不稳定,要慎用。 由于HotSpot虚拟机目前的实现方法导致栈上分配实现起来比较复杂,所以HotSpot虚拟机中暂时还没有这项优化。

②、相关的JVM参数

  • -XX:+DoEscapeAnalysis 开启逃逸分析
  • -XX:+PrintEscapeAnalysis 开启逃逸分析后,可通过此参数查看分析结果
  • -XX:+EliminateAllocations 开启标量替换
  • -XX:+EliminateLocks 开启同步消除
  • -XX:+PrintEliminateAllocations 开启标量替换后,查看标量替换情况。

猜你喜欢

转载自juejin.im/post/7126423182575140895