• JVM虚拟机浅谈(四)


    一、逃逸分析

    1.1 定义

    逃逸分析是“一种确定指针动态范围的方法,它可以分析在程序的哪些地方可以访问到指针。

    1.2 判定条件

    在 Java 虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否会逃逸。即时编译器判断对象逃逸的依据有两个:一是看对象是否被存入堆中,二是看对象是否作为方法调用的调用者或者参数。

    前者很好理解,一旦对象被存入堆中,其他线程便能获得该对象的引用。即时编译器因此无法追踪所有使用该对象的代码位置。

    关于后者,由于 Java 虚拟机的即时编译器是以方法为单位的,对于方法中未被内联的方法调用,即时编译器会将其当成未知代码,毕竟它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中。因此,我们可以认为方法调用的调用者以及参数是逃逸的。

    1.3 优化

    即时编译器会根据逃逸分析的结果进行优化,如锁消除以及标量替换。

    锁消除

    如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有意义。这是因为其他线程并不能获得该锁对象,因此也不可能对其进行加锁。在这种情况下,即时编译器可以消除对该不逃逸锁对象的加锁、解锁操作。

    关闭默认开启的逃逸分析,命令如下:

    java -XX:-DoEscapeAnalysis

    1. public class SynchronizeEscapeTest {
    2. public static void main(String[] args) {
    3. for (int i = 0; i < 10; i++) {
    4. long start = System.currentTimeMillis();
    5. for (int j = 0; j < 10_000_000; j++) {
    6. SynchronizeEscapeTest synchronizeEscapeTestc = new SynchronizeEscapeTest();
    7. synchronizeEscapeTestc.eliminate();
    8. }
    9. System.out.println(System.currentTimeMillis() - start + "ms");
    10. }
    11. }
    12. public void eliminate() {
    13. synchronized (new Object()) {
    14. }
    15. }
    16. }
    1. 耗时时长
    2. 255ms
    3. 237ms
    4. 187ms
    5. 232ms
    6. 148ms
    7. 128ms
    8. 142ms
    9. 210ms
    10. 171ms
    11. 211ms

    开启默认逃逸分析

    java -XX:+DoEscapeAnalysis

    1. 耗时时长
    2. 14ms
    3. 9ms
    4. 4ms
    5. 3ms
    6. 4ms
    7. 3ms
    8. 4ms
    9. 3ms
    10. 3ms
    11. 4ms

    synchronized (new Object()) {} 由于其他线程不能获得该锁对象,因此也无法基于该锁对象构造两个线程之间的 happens-before 规则

    synchronized (escapedObject) {}则不然。由于其他线程可能会对逃逸了的对象escapedObject进行加锁操作,从而构造了两个线程之间的 happens-before 关系。因此即时编译器至少需要为这段代码生成一条刷新缓存的内存屏障指令。

    问题一:

    开启逃逸分析后,

    synchronized (new Object()) {} 会不会在堆中创建对象实例?

    标量替换

    我们知道,Java 虚拟机中对象都是在堆上分配的,而堆上的内容对任何线程都是可见的。与此同时,Java 虚拟机需要对所分配的堆内存进行管理,并且在对象不再被引用时回收其所占据的内存

    如果逃逸分析能够证明某些新建的对象不逃逸,那么 Java 虚拟机完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象。不过,由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。所谓的标量,就是仅能存储一个值的变量,比如 Java 代码中的局部变量。与之相反,聚合量则可能同时存储多个值,其中一个典型的例子便是 Java 对象。

    foreach语法糖

    我们知道,Java 中Iterable对象的 foreach 循环遍历是一个语法糖,Java 编译器会将该语法糖编译为调用Iterable对象的iterator方法,并用所返回的Iterator对象的hasNext以及next方法,来完成遍历。

    1. public static void forEach(ArrayList<Object> list, Consumer<Object> f) {
    2. for (Object obj : list) {
    3. f.accept(obj);
    4. }
    5. }

    1. public static void foreach(java.util.ArrayList<java.lang.Object>, java.util.function.Consumer<java.lang.Object>);
    2. Code:
    3. 0: aload_0
    4. 1: invokevirtual #9 // Method java/util/ArrayList.iterator:()Ljava/util/Iterator;
    5. 4: astore_2
    6. 5: aload_2
    7. 6: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
    8. 11: ifeq 31
    9. 14: aload_2
    10. 15: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
    11. 20: astore_3
    12. 21: aload_1
    13. 22: aload_3
    14. 23: invokeinterface #12, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
    15. 28: goto 5
    16. 31: return

    等价实现代码

    1. public static void foreach(ArrayList<Object> list, Consumer<Object> f) {
    2. Iterator var2 = list.iterator();
    3. while(var2.hasNext()) {
    4. Object obj = var2.next();
    5. f.accept(obj);
    6. }
    7. }

    ArrayList$Itr

    1. public class ArrayList ... {
    2. public Iterator<E> iterator() {
    3. return new Itr();
    4. }
    5. private class Itr implements Iterator<E> {
    6. int cursor; // index of next element to return
    7. int lastRet = -1; // index of last element returned; -1 if no such
    8. int expectedModCount = modCount;
    9. ...
    10. public boolean hasNext() {
    11. return cursor != size;
    12. }
    13. @SuppressWarnings("unchecked")
    14. public E next() {
    15. checkForComodification();
    16. int i = cursor;
    17. if (i >= size)
    18. throw new NoSuchElementException();
    19. Object[] elementData = ArrayList.this.elementData;
    20. if (i >= elementData.length)
    21. throw new ConcurrentModificationException();
    22. cursor = i + 1;
    23. return (E) elementData[lastRet = i];
    24. }
    25. ...
    26. final void checkForComodification() {
    27. if (modCount != expectedModCount)
    28. throw new ConcurrentModificationException();
    29. }
    30. }
    31. }

    问题二:

    如果在热点代码中使用foreach循环,会不会对Java堆有压力。

    关闭默认开启的逃逸分析,命令如下:

    java -XX:-DoEscapeAnalysis

    -Xlog:gc

    1. public class EscapeTest {
    2. public static void main(String[] args) {
    3. ArrayList<Object> list = new ArrayList<>();
    4. for (int i = 0; i < 100; i++) {
    5. list.add(i);
    6. }
    7. for (int i = 0; i < 400_000_000; i++) {
    8. foreach(list,obj -> {});
    9. }
    10. }
    11. public static void foreach(ArrayList<Object> list, Consumer<Object> f) {
    12. for (Object obj : list) {
    13. f.accept(obj);
    14. }
    15. }
    16. }

    GC日志和耗时

    1. [0.013s][info][gc] Using G1
    2. [8.675s][info][gc] GC(126) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.199ms
    3. [8.738s][info][gc] GC(127) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.193ms
    4. [8.801s][info][gc] GC(128) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.192ms
    5. [8.866s][info][gc] GC(129) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.196ms
    6. [8.931s][info][gc] GC(130) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.197ms
    7. [8.994s][info][gc] GC(131) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.189ms
    8. [9.057s][info][gc] GC(132) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.187ms
    9. [9.121s][info][gc] GC(133) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.200ms
    10. [9.185s][info][gc] GC(134) Pause Young (Normal) (G1 Evacuation Pause) 93M->1M(154M) 0.189ms
    11. 8951ms

    开启默认逃逸分析

    java -XX:+DoEscapeAnalysis -Xlog:gc

    GC日志和耗时

    1. [0.014s][info][gc] Using G1
    2. 1910ms

    如果使用下面的代码会有什么问题

    1. public void forEach(ArrayList<Object> list, Consumer<Object> f) {
    2. for (int i = 0; i < list.size(); i++) {
    3. f.accept(list.get(i));
    4. }
    5. }

    理想情况下,即时编译器能够内联对ArrayList$Itr构造器的调用,对hasNext以及next方法的调用,以及当内联了Itr.next方法后,对checkForComodification方法的调用

    如果这些方法调用均能够被内联,那么结果将近似于下面这段伪代码:

    1. public void forEach(ArrayList<Object> list, Consumer<Object> f) {
    2. Itr iter = new Itr; // 注意这里是new指令
    3. iter.cursor = 0;
    4. iter.lastRet = -1;
    5. iter.expectedModCount = list.modCount;
    6. while (iter.cursor < list.size) {
    7. if (list.modCount != iter.expectedModCount)
    8. throw new ConcurrentModificationException();
    9. int i = iter.cursor;
    10. if (i >= list.size)
    11. throw new NoSuchElementException();
    12. Object[] elementData = list.elementData;
    13. if (i >= elementData.length)
    14. throw new ConcurrentModificationException();
    15. iter.cursor = i + 1;
    16. iter.lastRet = i;
    17. Object obj = elementData[i];
    18. f.accept(obj);
    19. }
    20. }

    可以看到,这里新建的ArrayList$Itr实例既没有被存入任何字段之中,也没有作为任何方法调用的调用者或者参数。因此,逃逸分析将断定该实例不逃逸。通过使用标量替换优化技术,可以把原本对对象的字段的访问,替换为一个个局部变量的访问。例如前面经过内联之后的 forEach 代码就可以被替换为如下代码:

    1. public void forEach(ArrayList<Object> list, Consumer<Object> f) {
    2. // Itr iter = new Itr; // 经过标量替换后该分配无意义,可以被优化掉
    3. int cursor = 0; // 标量替换
    4. int lastRet = -1; // 标量替换
    5. int expectedModCount = list.modCount; // 标量替换
    6. while (cursor < list.size) {
    7. if (list.modCount != expectedModCount)
    8. throw new ConcurrentModificationException();
    9. int i = cursor;
    10. if (i >= list.size)
    11. throw new NoSuchElementException();
    12. Object[] elementData = list.elementData;
    13. if (i >= elementData.length)
    14. throw new ConcurrentModificationException();
    15. cursor = i + 1;
    16. lastRet = i;
    17. Object obj = elementData[i];
    18. f.accept(obj);
    19. }
    20. }

    可以看到,原本需要在内存中连续分布的对象,现已被拆散为一个个单独的字段cursor,lastRet,以及expectedModCount。这些字段既可以存储在栈上,也可以直接存储在寄存器中。而该对象的对象头信息则直接消失了,不再被保存至内存之中。

    由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。与栈上分配相比,它对字段的内存连续性不做要求,而且,这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。

    部分逃逸分析

    C2编译器的逃逸分析与控制流无关,相对来说比较简单。Graal编译器则引入了一个与控制流有关的逃逸分析,名为部分逃逸分析。它解决了所新建的实例仅在部分程序路径中逃逸的情况。

    1. public static void bar(boolean condition) {
    2. Object foo = new Object();
    3. if (condition) {
    4. foo.hashCode();
    5. }
    6. }
    7. // 可以手工优化为:
    8. public static void bar(boolean condition) {
    9. if (condition) {
    10. Object foo = new Object();
    11. foo.hashCode();
    12. }
    13. }

    假设 if 语句的条件成立的可能性只有 1%,那么在 99% 的情况下,程序没有必要新建对象。其手工优化的版本正是部分逃逸分析想要自动达到的成果。

    部分逃逸分析将根据控制流信息,判断出新建对象仅在部分分支中逃逸,并且将对象的新建操作推延至对象逃逸的分支中。与 C2 所使用的逃逸分析相比,Graal 所使用的部分逃逸分析能够优化更多的情况,不过它编译时间也更长一些。

    测试Graal编译器的部分逃逸分析

    启用Graal

    -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -X:log:gc

    1. public class PartialEscapeTest {
    2. long placeHolder0;
    3. long placeHolder1;
    4. long placeHolder2;
    5. long placeHolder3;
    6. long placeHolder4;
    7. long placeHolder5;
    8. long placeHolder6;
    9. long placeHolder7;
    10. long placeHolder8;
    11. long placeHolder9;
    12. long placeHoldera;
    13. long placeHolderb;
    14. long placeHolderc;
    15. long placeHolderd;
    16. long placeHoldere;
    17. long placeHolderf;
    18. public static void bar(boolean condition) {
    19. PartialEscapeTest foo = new PartialEscapeTest();
    20. if (condition) {
    21. foo.hashCode();
    22. }
    23. }
    24. public static void main(String[] args) {
    25. for (int i = 0; i < Integer.MAX_VALUE; i++) {
    26. bar(i % 100 == 0);
    27. }
    28. }
    29. }

    GC日志

    1. [0.012s][info ][gc] Using G1
    2. [3.186s][info ][gc] GC(64) Pause Young (Normal) (G1 Evacuation Pause) 482M->5M(797M) 2.010ms
    3. [3.238s][info ][gc] GC(65) Pause Young (Normal) (G1 Evacuation Pause) 482M->5M(797M) 1.863ms
    4. [3.289s][info ][gc] GC(66) Pause Young (Normal) (G1 Evacuation Pause) 482M->5M(797M) 2.601ms
    5. [3.346s][info ][gc] GC(67) Pause Young (Normal) (G1 Evacuation Pause) 482M->5M(797M) 1.679ms
    6. [3.399s][info ][gc] GC(68) Pause Young (Normal) (G1 Evacuation Pause) 482M->5M(797M) 1.312ms
    7. [3.718s][info ][gc] GC(69) Pause Young (Normal) (G1 Evacuation Pause) 482M->6M(797M) 1.925ms
    8. [4.556s][info ][gc] GC(70) Pause Young (Normal) (G1 Evacuation Pause) 483M->6M(797M) 3.850ms
    9. [5.375s][info ][gc] GC(71) Pause Young (Normal) (G1 Evacuation Pause) 482M->7M(797M) 5.514ms
    10. [6.182s][info ][gc] GC(72) Pause Young (Normal) (G1 Evacuation Pause) 483M->7M(797M) 5.957ms
    11. [6.985s][info ][gc] GC(73) Pause Young (Normal) (G1 Evacuation Pause) 483M->7M(797M) 1.327ms
    12. [7.793s][info ][gc] GC(74) Pause Young (Normal) (G1 Evacuation Pause) 483M->7M(797M) 1.386ms

    如果不启用Graal

    -XX:+UnlockExperimentalVMOptions -X:log:gc

    GC日志

    1. [0.013s][info ][gc] Using G1
    2. [17.839s][info ][gc] GC(760) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.467ms
    3. [17.861s][info ][gc] GC(761) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.607ms
    4. [17.885s][info ][gc] GC(762) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.672ms
    5. [17.909s][info ][gc] GC(763) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.587ms
    6. [17.933s][info ][gc] GC(764) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.698ms
    7. [17.956s][info ][gc] GC(765) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.547ms
    8. [17.979s][info ][gc] GC(766) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.538ms
    9. [18.000s][info ][gc] GC(767) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.437ms
    10. [18.021s][info ][gc] GC(768) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.440ms
    11. [18.042s][info ][gc] GC(769) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.456ms
    12. [18.063s][info ][gc] GC(770) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.482ms
    13. [18.087s][info ][gc] GC(771) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.955ms
    14. [18.112s][info ][gc] GC(772) Pause Young (Normal) (G1 Evacuation Pause) 397M->1M(661M) 0.748ms

    问题三:

    如果经过即时编译器分析后对象不会发生逃逸,还会在堆中创建对象吗

    参考链接:深入拆解Java虚拟机_JVM_Java底层-极客时间

  • 相关阅读:
    数据库系统原理与应用教程(071)—— MySQL 练习题:操作题 110-120(十五):综合练习
    电脑模拟写字板应用设计(Java+Swing+Eclipse)
    springboot-mybatisplus-redis二级缓存
    软件测试面试题
    ICPC 2020沈阳站 - H. The Boomsday Project(dp,双指针优化)
    PAT 1050 String Subtraction
    在VScode中使用Jupyter Notebook的一些技巧
    【洛谷】P3377 【模板】左偏树(可并堆)
    .Net下验证MongoDB 的 Linq 模式联合查询是否可用
    『忘了再学』Shell流程控制 — 34、if条件判断语句(二)
  • 原文地址:https://blog.csdn.net/wuweiwoshishei/article/details/126401593