JVM-逃逸分析

JVM-逃逸分析

jvm学习记录

这个可以从一个问题入手:Java中的对象都是在堆中分配吗?说明为什么!
这个很明显,稍微会点JVM知识的人都会知道,但是JVM是怎么来判断是否应该在堆上分配还是其它地方分配?

主要有这两种逃逸情况,我们来聊一聊。

  • 方法逃逸
  • 线程逃逸

1.什么是逃逸分析技术

逃逸分析: 就是分析Java对象的动态作用域,如果在一个方法内创建了一个对象,而且外部对象能够引用到这个对象的话,这就是方法逃逸 (作用域逃出了这个方法) ,如果能够被其它线程使用到的话,这就是线程逃逸(作用域逃出了这个线程)

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

	//下面两个方法,将创建的对象 s 返回,这样可以被其他方法或线程引用。
	public String getStr(){
    
    
        String s = "这是个测试案例";
        return s;
    }
    public StringBuilder getStr1(){
    
    
        StringBuilder s = new StringBuilder();
        s.append("这是个测试案例");
        return s;
    }
    //下面就没有逃逸出这个方法
    public String getStr2(){
    
    
        StringBuilder s = new StringBuilder();
        s.append("这是个测试案例");
        return s.toString();
    }

设置逃逸分析的 JVM 参数如下:

  • 开启逃逸分析:-XX:+DoEscapeAnalysis
  • 关闭逃逸分析:-XX:-DoEscapeAnalysis
  • 显示分析结果:-XX:+PrintEscapeAnalysis

从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

2.逃逸分析算法

个人能力有限,下面这段全靠百度。

Java Hotspot 编译器实现下面论文中描述的逃逸算法:

[Choi99] Jong-Deok Choi, Manish Gupta, Mauricio Seffano,
Vugranam C. Sreedhar, Sam Midkiff,
“Escape Analysis for Java”, Procedings of ACM SIGPLAN
OOPSLA Conference, November 1, 1999

根据 Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C.
Sreedhar, Sam Midkiff 等大牛在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。

该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在次基础上,提出一种组合数据流分析法。

由于算法是上下文相关和流敏感的,并且模拟了对象任意层次的嵌套关系,所以分析精度较高,只是运行时间和内存消耗相对较大。

3.逃逸分析优化

利用逃逸分析,编译器可以进行如下优化:

  • 同步消除,也叫锁消除。
  • 标量替换
  • 栈上分配

3.1、同步消除(锁消除)

在JIT编译过程中,如果发现一个对象不会被其它线程(多线程)访问到,那么针对这个对象的同步措施就可以省略掉,即「锁销除」。例如 Vector 和 StringBuffer ,它们中的很多方法都是加了锁的,当某个对象确定是线程安全的情况下,JIT编译器会在编译这段代码时进行锁消除来提升效率。JDK8中,同步消除默认开启。

就像下面这个方法

public void func() {
    
    
    Object o = new Object();
    synchronized(o) {
    
    
        System.out.println(o);
    }
}

代码中对o这个对象进行加锁,但是o对象的生命周期只在func()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成如下:

public void func() {
    
    
    Object o= new Object();
    System.out.println(o);
}

或者

//代码中createStringBuffer方法中的局部对象buf,
//就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,
//都会创建不同的buf 对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源
//在开启逃逸分析与锁消除后,会自动将锁消除掉,减少每次获取锁的开销
public static String createStringBuffer(String str1, String str2) {
    
    
        StringBuffer buf = new StringBuffer();
        buf.append(str1);// append方法是同步操作,里面加了synchronized
        buf.append(str2);
        return buf.toString();
    }

这种优化是很好的,避免了对象加锁解锁的各种开销。

-server //运行在server模式
-XX:+DoEscapeAnalysis //开启逃逸分析
-XX:+EliminateLocks  //开启锁消除

3.2、标量替换

首先要明白标量和聚合量
「标量(Scalar)」是指无法再分解成更小粒度的数据,例如 Java 中的原始数据类型(int,long 等),相对如果一个数据可以继续分解,则称之为「聚合量(Aggregate)」,例如 Java对象,它可以将其成员变量分解。在 JIT 编译过程中,经过逃逸分析确定一个对象不会被其他线程或者方法访问,那么会将对象的创建替换成为多个成员变量的创建,称之为「标量替换」。

假如我们只用到了它的部分属性的话就只需要在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。

如下代码演示:

public static void main(String[] args) {
    
    
   test();
}

private static void test() {
    
    
   Point p = new Point1,2;
   System.out.println("p.x="+p.x+"; p.y="+p.y);
}
class Point{
    
    
    private int x;
    private int y;
}

以上代码中,p对象并没有逃逸出test方法,并且p对象是可以拆解成标量的。那么,JIT就会不会直接创建Point对象,而是直接使用两个标量int x ,int y来替代Point对象。
它可能就会变成以下:

private static void test() {
    
    
   int x = 1;
   int y = 2;
   System.out.println("p.x="+x+"; p.y="+y);
}

标量替换减少了对象创建需要的内存开销,可以减少GC的频率

3.3、栈上分配

「栈上分配」是指对象和数据不是创建在堆上,而是创建在栈上,随着方法的结束自动销毁。但实际上,JVM 例如常用的「HotSpot」虚拟机并没有实现栈上分配,实际是用「标量替换」代替实现的。

4.线程逃逸

简单点说就是:这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。对象逃出了当前线程。

线程中创建对象会首先在TLAB上尝试创建(这个是在堆上年轻代的Eden中的),但 TLAB 通常都很小,所以对象相对比较大的时候,会在 Eden 区的共享区域进行分配。

TLAB 的全称是 Thread Local Allocation Buffer,JVM 默认给每个线程开辟一个 buffer 区域,用来加速对象分配。这个 buffer 就放在 Eden 区中。而没有分配的Eden区域就是共享区域了。

猜你喜欢

转载自blog.csdn.net/qq_41257365/article/details/107584851