如果面試官問你 JVM,額外回答逃逸分析技術會讓你加分!

陳皮的JavaLib 發表於 2021-07-16
面試 JVM

我是陳皮,一個在網際網路 Coding 的 ITer,微信搜尋「陳皮的JavaLib」第一時間閱讀最新文章。

引言

我在面試別人的過程中,JVM 記憶體模型我幾乎必問,雖然有人說問這些就是面試造航母,工作擰螺絲。如果你想當一名 CRUD 碼農,你可以選擇不用瞭解這些。

在 JVM 記憶體模型的問答中,有些人能說出物件是在堆上分配的。但當我問物件一定是在堆上儲存的嘛時,大部分人都回答是,或者猶豫了。

其實能回答出物件是在堆上分配儲存已算正確了。但隨著 JIT 即時編譯器的發展和逃逸分析技術的逐漸成熟,所有物件都分配到堆上也逐漸變得不那麼絕對了。棧上分配標量替換鎖消除等優化技術會發生一些微妙的變化。

我們知道,我們編寫的 Java 原始碼通過 javac 編譯成位元組碼檔案,然後類載入器將位元組碼檔案載入到記憶體中,JVM 逐行讀取解釋位元組碼翻譯成對應的機器指令執行。很明顯,解釋執行比那些可直接執行的二進位制程式(例如 C 語言程式)慢得多。

所以為了提高效率,引入了 JIT (即時編譯器)優化技術。Java 程式還是會通過直譯器進行解釋執行,但是如果某個方法或者程式碼塊執行比較頻繁的時候,JVM 認為這是熱點程式碼,然後將熱點程式碼翻譯成本地機器指令,並且進行優化,快取起來,下次再執行此段程式碼的時候直接執行而不用再解釋。

JIT 中一個很重要的優化技術就是逃逸分析(Escape Analysis)。

逃逸分析

逃逸分析,其實就是分析一個物件是否會逃逸出方法,分析物件的動態作用域。如果一個物件在一個方法內定義,並且有可能被方法外部引用使用,那認為它逃逸了。

例如以下的 person 物件就發生了逃逸,即有可能會被方法外部引用。

public Person personEscape() {
  Person person = new Person();
  return person;
}

所以為什麼要進行逃逸分析,其實最終目的就是為程式做優化,提高執行效能。有如下優化技術點:

  • 棧上分配
  • 標量替換
  • 鎖消除

JDK1.7 開始,逃逸分析預設是開啟的,可以通過以下引數進行啟停。

# 開啟
-XX:+DoEscapeAnalysis
# 關閉
-XX:-DoEscapeAnalysis

棧上分配

如果分析一個物件沒有逃逸出方法的時候,就有可能被分配到棧上。這樣就不需要在堆中進行 GC 回收,提高了效能。

package com.chenpi;

/**
 * @Description
 * @Author 陳皮
 * @Date 2021/7/14
 * @Version 1.0
 */
public class EscapeAnalysisTest {

  public static void main(String[] args) {

    long startTime = System.currentTimeMillis();

    for (int i = 0; i < 10000000; i++) {
      stackAlloc();
    }

    System.out.println((System.currentTimeMillis() - startTime) + "ms");
  }

  public static void stackAlloc() {
    Person person = new Person("陳皮", 18);
  }

}

class Person {

  private String name;
  private long age;

  public Person(String name, long age) {
    this.name = name;
    this.age = age;
  }
}

虛擬機器引數設定開啟逃逸分析,並且列印 GC 日誌。

-Xms200m -Xmx200m -XX:+DoEscapeAnalysis -XX:+PrintGC

執行程式結果如下,消耗只需要 10 ms,並且沒有 GC 。

10ms

關閉逃逸分析,並且列印 GC 日誌。

-Xms200m -Xmx200m -XX:-DoEscapeAnalysis -XX:+PrintGC

執行程式結果如下,消耗時間增加了10多倍,並且伴隨著多次的 GC 。

[GC (Allocation Failure)  51712K->784K(196608K), 0.0050396 secs]
[GC (Allocation Failure)  52496K->784K(196608K), 0.0030730 secs]
[GC (Allocation Failure)  52496K->752K(196608K), 0.0013993 secs]
[GC (Allocation Failure)  52464K->720K(196608K), 0.0018371 secs]
176ms

標量替換

  • 標量:不可再分解成更小資料的型別,例如基本資料型別就是標量。
  • 聚合量:可以再分解成其他聚合量或者標量的資料型別,例如物件引用型別。

如果一個物件不會發生逃逸,那麼 JIT 可以優化把這個物件分解成若干個標量來代替。這就是標量替換。

public void scalarReplace() {
  Coordinates coordinates = new Coordinates(105.10, 80.22);
  System.out.println(coordinates.longitude);
  System.out.println(coordinates.latitude);
}

以上演示程式,coordinates 物件不會發生逃逸,所以 JIT 編譯器可以使用標量替換進行優化。最終被優化成如下程式。

public void scalarReplace() {
  System.out.println(105.10);
  System.out.println(80.22);
}

其實在現有的虛擬機器中,並沒有真正的實現棧上分配,其實是通過標量替換來實現的。

鎖消除

為什麼要消除鎖呢?因為加鎖會降低效能,那如何不用加鎖是最好的。如果分析出加鎖的物件不會發生逃逸,即只能被一個執行緒訪問,JIT 是可以優化消除這個鎖的。也稱為同步省略。

public void lockRemove() {
  synchronized (new Object()) {
    System.out.println("我是陳皮!");
  }
}

以上演示程式,Object 物件不會發生逃逸,所以也只能當前執行緒訪問到,所以 JIT 編譯器可以進行優化鎖消除。最終被優化成如下程式。

public void lockRemove() {
  System.out.println("我是陳皮!");
}

總結

但隨著 JIT 即時編譯器的發展和逃逸分析技術的逐漸成熟,所有物件都分配到堆上也逐漸變得不那麼絕對了。通過逃逸分析技術,物件可能被分配到棧上,能減少 GC,提高程式效能。

但是開啟逃逸分析的程式的效能一定高於沒有開啟逃逸分析的效能嗎?其實不一定。逃逸分析技術其實也是很複雜的,所以也是一個會耗時的過程,如果經過逃逸分析之後,發現所有物件都逃逸了,就不能做優化處理,那這個逃逸分析的過程就消耗了時間,還不起優化作用,得不償失。