1. 程式人生 > >Java-JVM-逃逸分析

Java-JVM-逃逸分析

Java-JVM-逃逸分析

摘要

逃逸分析其實並不是新概念,早在1999年就有論文提出了該技術。但在Java中算是新穎而前言的優化技術,從JDK1.6才開始引入該技術。本文會簡單說說他是怎麼操作的。

0x01 逃逸分析

1.1 輔助優化

首先我們要認識到,逃逸分析並不是直接優化的技術,而是作為其他優化的依據。

1.2 分析物件動態作用域

1.2.1 方法逃逸

  • 定義
    一個物件在方法中被定義,但卻被方法以外的其他程式碼使用。
  • 場景
    如傳參等可能導致此情況發生。
  • 例子:

1.2.2 執行緒逃逸

  • 定義
    一個物件由某個執行緒在方法中被定義,但卻被其他執行緒訪問。
  • 場景
    如類變數、公用的或有get、set方法的例項變數等

1.2.3 逃逸分析過程

  • 資料流敏感的若干複雜分析,以確定程式各分支執行對目標物件影響,耗時較長。

1.3 JVM配置

逃逸分析具體配置項如下:

  • 開啟逃逸分析(JDK8中,逃逸分析預設開啟。)
    -XX:+DoEscapeAnalysis
  • 關閉逃逸分析
    -XX:-DoEscapeAnalysis
  • 逃逸分析結果展示
    -XX:+PrintEscapeAnalysis

0x02 物件優化

如果物件不會發生前述方法逃逸和執行緒逃逸情況(即完全不可能被別的方法和執行緒訪問到的物件),JVM可做以下優化:

2.1 棧上分配

  • 普通物件在堆中分配,各執行緒共享。但有GC消耗。
  • 當確定物件不會發生方法逃逸時,可線上程棧上分配物件。此時物件生命週期和方法相同,隨棧幀出棧時即可銷燬,不需要GC了。

2.2 同步消除

2.2.1 基本概念

  • 執行緒同步有效能消耗
  • 鎖消除:當確定物件不會發生執行緒逃逸時,可消除該物件不必要的同步操作(永不會競爭)。具體來說,JVM在編譯器執行時會掃描程式碼,當檢查到那些不可能存在共享區競爭,但卻有互斥同步的程式碼,直接將這樣的多此一舉的同步消除
  • 鎖粗化:JVM針對那些反覆在一段程式碼中對同一物件加鎖的情況,將同步鎖放在最外層包住這裡面的多次同步鎖,同時取消內部的同步鎖

2.2.2 JVM配置

(JDK8中,同步消除預設開啟。)
-XX:+EliminateLocks

2.3 標量替換

2.3.1 基本概念

  • 標量
    標量指無法分解的資料,如java中的基本資料型別及引用型別
  • 聚合量
    可以分解的,成為聚合量,如物件
  • 標量替換
    如果一個可拆分物件不會發生逃逸,那在程式執行時並不建立他,而是根據情況線上程棧上只建立用到的成員標量
  • 好處
    1. 原始物件標量替換後,往往可以只建立所需標量,節約了空間和時間。
    2. 此外,JVM可將這類成員變數放在棧上,乃至移動到高速暫存器中進行讀寫,大大提升讀寫效率。
    3. 還可以進一步優化。

截止JDK8,棧上直接分配物件並未實現,而是將物件標量替換後在棧上分配。

2.3.2 JVM配置

  • 開啟標量替換(JDK8中,逃逸分析預設開啟。)
    -XX:+EliminateAllocations
  • 檢視標量替換詳情
    -XX:+PrintEliminateAllocations

0x03 實驗

3.1 標量替換

Java程式碼如下

public class EscapeDemo
{
    class Person
    {
        private String name;
        private int age;

        public String getName()
        {
            return name;
        }

        public void setName(String name)
        {
            this.name = name;
        }

        public int getAge()
        {
            return age;
        }

        public void setAge(int age)
        {
            this.age = age;
        }
    }

    public void escapeTest(){
        for(int i = 0 ; i < 1000000 ; i++){
            Person person = new Person();
            person.setAge(i);
        }
    }

    public static void main(String[] args)
            throws IOException
    {
        long startTime = System.currentTimeMillis();
        new EscapeDemo().escapeTest();
        long endTime = System.currentTimeMillis();
        System.out.println("elapsed time = "+ (endTime - startTime));
        System.in.read();
    }
}

可以看到,這是段簡單的程式碼,就是在escapeTest方法中反覆建立Person物件100萬次。

3.1.1 不使用逃逸分析

命令如下:

java -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo

結果如下:

elapsed time = 22

$ jmap -histo 18938
 num     #instances         #bytes  class name
----------------------------------------------
   1:       1000000       24000000  demos.jvm.escape.EscapeDemo$Person

此時耗時較慢,22ms,並實打實的構建了100萬個物件。

3.1.2 不使用標量替換

命令如下:

java -Xmx1G -XX:-EliminateAllocations -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo

結果如下:

elapsed time = 23

$ jmap -histo 18935
 num     #instances         #bytes  class name
----------------------------------------------
   1:       1000000       24000000  demos.jvm.escape.EscapeDemo$Person

此時情況和不開啟逃逸分析時情況差不多,耗時較慢,23ms,並實打實的構建了100萬個物件。

3.1.3 使用逃逸分析和標量替換

命令如下(JDK8預設開啟逃逸分析和標量替換):

java -Xmx1G -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo

結果如下:

elapsed time = 7

$ jmap -histo 1839
 num     #instances         #bytes  class name
----------------------------------------------
   1:        193418        4642032  demos.jvm.escape.EscapeDemo$Person

使用了逃逸分析和標量替換後,程式執行僅耗時7毫秒,是前面不開啟時的三分之一還少,且EscapeDemo只分配了13萬多個物件。

因為此時,JVM逃逸分析EscapeDemo只會在escapeTest方法中執行,不會發生方法逃逸和執行緒逃逸,所以可以對部分聚合量EscapeDemo進行標量替換,將拆分後的標量在棧上分配,減少直接在堆上分配的物件數量。

3.2 同步消除

3.2.1 鎖粗化

  • Java程式碼如下:
/**
 * Created by chengc on 2018/12/23.
 * 鎖粗化
 */
public class EliminateLocks2
{
    public StringBuilder concatStr2(String ... strs){
        StringBuilder sb = new StringBuilder();
        for (String string : strs) {
            synchronized (sb) {
                sb.append(string + " ");
            }
        }
        return sb;
    }

    public static void main(String[] args)
            throws IOException
    {
        long startTime = System.currentTimeMillis();
        EliminateLocks2 eliminateLocks = new EliminateLocks2();

        for(int i = 0 ; i < 1000000 ; i++){
            StringBuilder sb = eliminateLocks.concatStr2("A", "B", "C");
            String result = sb.toString();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("elapsed time = "+ (endTime - startTime));
        System.in.read();
    }
}
  • 不開啟同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks2
[GC (Allocation Failure) [PSYoungGen: 65536K->432K(76288K)] 65536K->440K(251392K), 0.0007763 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->448K(76288K)] 65976K->464K(251392K), 0.0010218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->352K(76288K)] 66000K->376K(251392K), 0.0008053 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 174
  • 開啟同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks2
[GC (Allocation Failure) [PSYoungGen: 65536K->448K(76288K)] 65536K->456K(251392K), 0.0008636 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->448K(76288K)] 65992K->464K(251392K), 0.0007134 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->432K(76288K)] 66000K->448K(251392K), 0.0007321 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 146
  • 結果分析
  1. 無鎖消除
    StringBuilder類的例項sb物件在concatStr2方法中被建立,並在最後被返回到方法外部,且會被呼叫方賦值給變數result所以此時sb物件會發生方法逃逸,不能進行同步鎖消除優化
  2. 鎖粗化
    for迴圈內部對sb物件進行synchronized修飾,反覆對sb物件使用同步鎖。此時會進行鎖粗化優化,執行時間由174ms縮短為146ms。

3.2.2 鎖消除

  • Java程式碼如下:
/**
 * Created by chengc on 2018/12/23.
 * 鎖消除
 */
public class EliminateLocks3
{
    public String concatStr3(String ... strs){
        StringBuilder sb = new StringBuilder();
        synchronized (sb){
            for (String string : strs) {
                    sb.append(string+" ");
            }
        }
        return sb.toString();
    }

    public static void main(String[] args)
            throws IOException
    {
        long startTime = System.currentTimeMillis();
        EliminateLocks3 eliminateLocks = new EliminateLocks3();

        for(int i = 0 ; i < 1000000 ; i++){
            String result = eliminateLocks.concatStr3("A", "B", "C");
        }

        long endTime = System.currentTimeMillis();
        System.out.println("elapsed time = "+ (endTime - startTime));
        System.in.read();
    }
}
  • 不開啟同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks3
[GC (Allocation Failure) [PSYoungGen: 65536K->416K(76288K)] 65536K->424K(251392K), 0.0007494 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->448K(76288K)] 65960K->464K(251392K), 0.0010933 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->416K(76288K)] 66000K->432K(251392K), 0.0005035 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 146
  • 開啟同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks3
[GC (Allocation Failure) [PSYoungGen: 65536K->416K(76288K)] 65536K->424K(251392K), 0.0007823 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->448K(76288K)] 65960K->464K(251392K), 0.0009626 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->368K(76288K)] 66000K->384K(251392K), 0.0005893 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
elapsed time = 118
  • 結果分析
  1. 鎖消除
    StringBuilder類的例項sb物件在concatStr3方法中被建立,但在最後被返回到方法外部時最終返回的是轉換後的String物件而不是sb本身。所以此時sb物件不會發生方法逃逸,可以進行同步鎖消除優化。
  2. 無鎖粗化
    for迴圈外部對sb物件進行synchronized修飾,此時不會進行鎖粗化優化,執行時間由146ms縮短為118ms。

3.2.3 鎖粗化+鎖消除

Java程式碼如下:

public class EliminateLocks
{
    public String concatStr1(String ... strs){
        StringBuilder sb = new StringBuilder();
        for (String string : strs) {
            synchronized (sb) {
                sb.append(string + " ");
            }
        }
        return sb.toString();
    }

    public static void main(String[] args)
            throws IOException
    {
        long startTime = System.currentTimeMillis();
        EliminateLocks eliminateLocks = new EliminateLocks();

        for(int i = 0 ; i < 1000000 ; i++){
            String result = eliminateLocks.concatStr1("A", "B", "C");
        }

        long endTime = System.currentTimeMillis();
        System.out.println("elapsed time = "+ (endTime - startTime));
        System.in.read();
    }
}

注意,這次concatStr1方法返回的是toString,而且synchronized放在了for迴圈內部。

  • 不開啟同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks
[GC (Allocation Failure) [PSYoungGen: 65536K->448K(76288K)] 65536K->456K(251392K), 0.0011907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->432K(76288K)] 65992K->448K(251392K), 0.0009904 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->464K(76288K)] 65984K->480K(251392K), 0.0007127 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 195
  • 開啟同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks
[GC (Allocation Failure) [PSYoungGen: 65536K->432K(76288K)] 65536K->440K(251392K), 0.0007056 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->416K(76288K)] 65976K->432K(251392K), 0.0007054 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->416K(76288K)] 65968K->432K(251392K), 0.0005523 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 128
  • 結果分析
  1. 鎖消除
    StringBuilder類的例項sb物件在concatStr1方法中被建立,但在最後被返回到方法外部時最終返回的是轉換後的String物件而不是sb本身。所以此時sb物件不會發生方法逃逸,可以進行同步鎖消除優化。
  2. 鎖粗化
    for迴圈內部對sb物件進行synchronized修飾,反覆對sb物件使用同步鎖。此時會進行鎖粗化優化,執行時間由195ms縮短為128ms。

0x04 現存問題

現在最大的問題就是無法保證逃逸分析的收益大於進行此操作帶來的效能消耗。所以JVM目前採用不準確、時間較短演算法進行逃逸分析,以權衡收益和開銷。

0xFF 參考文件

《深入理解Java虛擬機器》

JAVA逃逸分析、棧上分配、標量替換、同步消除