代码优化是一个涉及面很广的“工程”,但是今天呢,本姑娘主要给大家分享基于逃逸分析,如何给代码做优化。那么逃逸分析是什么呢?我前面的文章也仔细的讲解过了,这里就不过多的赘述了。有不明白逃逸分析的可以先看我的这篇文章:欧尼酱讲JVM(14)——堆
目录
规避逃逸,优化代码
栈上分配
什么是栈上分配
常见的栈上分配的场景
通过对比体验栈上分配的好处
同步省略
什么是同步省略
通过对比体会省略前后的不同
标量替换
什么是标量替换
代码演示是如何替换的
标量替换参数设置
实战演示标量替换前后的性能对比
逃逸分析小结
规避逃逸,优化代码
规避逃逸后,编译器可以对代码做以下优化
- 栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
- 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
以上都是编译器做的,我们需要做的是尽量让对象不逃逸出方法,那么要如何做呢?请听小编为大家娓娓道来。
栈上分配
什么是栈上分配
JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话(也就是说这个对象没有被return出去),就可能被优化成栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
通俗点讲,对象没有被return出去,那么在创建对象的时候,对象的实例就不会被分配到堆空间,而是直接分配在栈中。我们知道,几乎所有对象的实例是都被分配到堆上的,这样的话,当这个对象没有被指针指向的时候,而且堆空间不足的时候,就会进行GC,这时候就需要STW,如果对象被频繁的创建在堆中,那么就需要频繁的GC,这种频繁的STW,无疑会降低性能。而栈上分配就是为了减少GC次数,提高性能。
常见的栈上分配的场景
在本仙女的其他博客中也已经说明了,分别是:
- 给成员变量复制
- 方法返回值
- 实例引用传递
通过对比体验栈上分配的好处
如果我只是这样干巴巴的讲,你们也只是这样干巴巴的听,这样的话,我怕噎着你们了,所以本姑娘决定润色一下,来点代码,实践一下。哈哈,本仙女还是很贴心的吧,废话少说,我要三连(点赞+收藏+关注)!
未发生逃逸的情况:
首先需要把参数设置一下,设置为不开启逃逸分析,这样的话对象实例就会全部在堆中创建。参数设置如下:
public class EscapeAnalysis {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0;i< 10000000;i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start) + "ms");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User{
}
}
执行时间如下:
可以从JVisualVM上看到,创建的实例数是10万个:
发生逃逸的情况:
首先设置参数,开启逃逸分析:
还是上面那段代码,我们看下执行时间:
哦买噶,漂亮,7ms, 仅仅是7ms,我们看到开启逃逸分析的选手想开了挂一样的快,仅仅7ms就结束了这一次的巅峰对决,果然不出我门所望,开启逃逸分析的选手以7ms的成绩获得了这次比赛的冠军!那么创建的实例数是多少呢,让我门拭目以待:
同步省略
什么是同步省略
要想了解什么是同步省略,就要先了解什么是线程同步:
- 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作而是处于等待状态,直到该线程完成操作, 其他线程才能对该内存地址进行操作,这一过程就叫做线程同步。
线程同步的代价是相当高的,同步的后果是降低并发性和性能。
- 同步省略:在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问,而没有发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫做同步省略,也叫锁消除。
通过对比体会省略前后的不同
同步省略前:
public class SynchronizedTest {
public void f(){
Object find = new Object();
synchronized (find){
System.out.println(find);
}
}
}
代码中对find这个对象进行了加锁,但是find对象的生命周期只在f()方法中,并不会被其他线程所访问,所以在JIT编译阶段就会被自动优化掉,优化成:
同步省略后:
public class SynchronizedTest {
public void f(){
Object find = new Object();
System.out.println(find);
}
}
标量替换
什么是标量替换
- 标量(Scalar):是指无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
- 聚合量(Aggregate):相对的,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量,因为它可以分解为其他的聚合量和标量。
- 标量替换:在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个成员变量(拆解后的成员变量必须是对象中包括的成员变量)来代替,这个过程就是标量替换。
代码演示是如何替换的
标量替换前:
public static void main(String[] args) {
find();
}
private static void find(){
Point point = new Point(1,2);
System.out.println("point.x = " + point.x + ";point.y = " + point.y);
}
public class Point {
private int x;
private int y;
}
标量替换后:
private static void find(){
int x = 1;
int y = 2;
System.out.println("point.x = " + x + ";point.y = " + y);
}
对标量替换过程的分析:
可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配内存了。则1和2就放到了栈针中。大大减少了堆空间的占用,也减少了GC。
标量替换为栈上分配提供了很好的基础。
标量替换参数设置
那么如何让JIT编译器自动为我们的程序进行标量替换呢?
-XX:+EliminateAllocations
设置以上参数打开标量替换,允许将对象打散分配到栈上。
实战演示标量替换前后的性能对比
不开启标量替换:
-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
public class ScalarReplace {
public static class User{
public int id;
public String name;
}
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "清酒欧尼酱";
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start) + "ms");
}
}
我们可以看到发生了很多次GC
开启标量替换:
-Xms100m -Xmx100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
运行上面的代码,可以看到:
没有发生GC,并且时间只有4ms。
逃逸分析小结
关于逃逸分析的论文在1999年就已经发表了,但是指导JDK1.6才有实现,并且这项技术到如今也不是十分成熟。期根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗,虽然经过逃逸分析可以做标量替换、栈上分配和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的,就这个逃逸分析的过程就白白浪费掉了。
虽然 这项技术不是很成熟,但是它也是即时编译优化技术中一个十分重要的手段。
注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择,据我所知,Oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关文档里已经说明,所以可以明确所有的对象实例都是创建在堆上的。
目前很多书籍还是基于JDK1.7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据取代,但是intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上的。