时间:2023-06-29 06:18:01 | 来源:网站运营
时间:2023-06-29 06:18:01 来源:网站运营
JVM系列之:关于逃逸分析的学习:上文讲解完方法内联后,JIT 即时编译还有一个最前沿的优化技术:逃逸分析(Escape Analysis) 。废话少说,我们直接步入正题吧。//StringBuffer对象发生了方法逃逸public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } public static String createString(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
关于逃逸分析技术,本人想过用代码展示对象是否发生了逃逸,比如说上述代码,根据理论知识可以认为 createStringBuffer 方法中发生了逃逸,但是具体是个什么情况,咱们都不清楚。虽然 JVM 有个参数 PrintEscapeAnalysis 可以显示分析结果,但是该参数仅限于 debug 版本的 JDK 才可以进行调试,多次尝试后,未能编译出 debug 版本的 JDK,暂且没什么思路,所以查看逃逸分析结果这件事先往后放一放,后续学习 JVM 调优再进一步来学习。-XX:+EliminateLocks
(默认开启)可以开启同步消除。 这个取消同步的过程就叫同步消除,也叫锁消除。@Getterpublic class Worker { private String name; private double money; public Worker() { } public Worker(String name) { this.name = name; } public void makeMoney() { money++; }}
测试代码如下:public class SynchronizedTest { public static void work(Worker worker) { worker.makeMoney(); } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); Worker worker = new Worker("hresh"); new Thread(() -> { for (int i = 0; i < 20000; i++) { work(worker); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 20000; i++) { work(worker); } }, "B").start(); long end = System.currentTimeMillis(); System.out.println(end - start); Thread.sleep(100); System.out.println(worker.getName() + "总共赚了" + worker.getMoney()); }}
执行结果如下:52hresh总共赚了28224.0
可以看出,上述两个线程同时修改同一个 Worker 对象的 money 数据,对于 money 字段的读写发生了竞争,导致最后结果不正确。像上述这种情况,即时编译器经过逃逸分析后认定对象发生了逃逸,那么肯定不能进行同步消除优化。//JVM参数:-Xms60M -Xmx60M -XX:+PrintGCDetails -XX:+PrintGCDateStampspublic class SynchronizedTest { public static void lockTest() { Worker worker = new Worker(); synchronized (worker) { worker.makeMoney(); } } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); new Thread(() -> { for (int i = 0; i < 500000; i++) { lockTest(); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 500000; i++) { lockTest(); } }, "B").start(); long end = System.currentTimeMillis(); System.out.println(end - start); }}
输出结果如下:56Heap PSYoungGen total 17920K, used 9554K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000) eden space 15360K, 62% used [0x00000007bec00000,0x00000007bf5548a8,0x00000007bfb00000) from space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) to space 2560K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007bfd80000) ParOldGen total 40960K, used 0K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000) object space 40960K, 0% used [0x00000007bc400000,0x00000007bc400000,0x00000007bec00000) Metaspace used 4157K, capacity 4720K, committed 4992K, reserved 1056768K class space used 467K, capacity 534K, committed 640K, reserved 1048576K
在 lockTest 方法中针对新建的 Worker 对象加锁,并没有实际意义,经过逃逸分析后认定对象未逃逸,则会进行同步消除优化。JDK8 默认开启逃逸分析,我们尝试关闭它,再看看输出结果。-Xms60M -Xmx60M -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+PrintGCDateStamps
输出结果变为:732022-03-01T14:51:08.825-0800: [GC (Allocation Failure) [PSYoungGen: 15360K->1439K(17920K)] 15360K->1447K(58880K), 0.0018940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap PSYoungGen total 17920K, used 16340K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000) eden space 15360K, 97% used [0x00000007bec00000,0x00000007bfa8d210,0x00000007bfb00000) from space 2560K, 56% used [0x00000007bfb00000,0x00000007bfc67f00,0x00000007bfd80000) to space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) ParOldGen total 40960K, used 8K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000) object space 40960K, 0% used [0x00000007bc400000,0x00000007bc402000,0x00000007bec00000) Metaspace used 4153K, capacity 4688K, committed 4864K, reserved 1056768K class space used 466K, capacity 502K, committed 512K, reserved 1048576K
经过对比发现,关闭逃逸分析后,执行时间变长,且内存占用变大,同时发生了垃圾回收。public class ScalarTest { public static double getMoney() { Worker worker = new Worker(); worker.setMoney(100.0); return worker.getMoney() + 20; } public static void main(String[] args) { getMoney(); }}
经过逃逸分析,Worker 对象未逃逸出 getMoney()的调用,因此可以对聚合量 worker 进行分解,得到局部变量 money,进行标量替换后的伪代码:public class ScalarTest { public static double getMoney() { double money = 100.0; return money + 20; } public static void main(String[] args) { getMoney(); }}
对象拆分后,对象的成员变量改为方法的局部变量,这些字段既可以存储在栈上,也可以直接存储在寄存器中。标量替换因为不必创建对象,减轻了垃圾回收的压力。-XX:+EliminateAllocations
可以开启标量替换(默认是开启的), -XX:+PrintEliminateAllocations
(同样需要debug版本的JDK)查看标量替换情况。public static void bar(boolean cond) { Object foo = new Object(); if (cond) { foo.hashCode(); }}// 可以手工优化为:public static void bar(boolean cond) { if (cond) { Object foo = new Object(); foo.hashCode(); }}
假设 if 语句的条件成立的可能性只有 1%,那么在 99% 的情况下,程序没有必要新建对象。其手工优化的版本正是部分逃逸分析想要自动达到的成果。public class PartialEscapeTest { long placeHolder0; long placeHolder1; long placeHolder2; long placeHolder3; long placeHolder4; long placeHolder5; long placeHolder6; long placeHolder7; long placeHolder8; long placeHolder9; long placeHoldera; long placeHolderb; long placeHolderc; long placeHolderd; long placeHoldere; long placeHolderf; public static void foo(boolean flag) { PartialEscapeTest o = new PartialEscapeTest(); if (flag) { o.hashCode(); } } public static void main(String[] args) { for (int i = 0; i < 1000000; i++) { foo(false); } }}
本次测试选用的是 JDK11,开启 Graal 编译器需要配置如下参数:-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
分别输出使用 C2 编译器或 Graal 编译器的 GC 日志,对应命令为:java -Xlog:gc* PartialEscapeTestjava -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -Xlog:gc* PartialEscapeTest
通过对比 GC 日志可以发现内存占用情况不一致,Graal 编译器下内存占用更小一点。[0.012s][info][gc,heap] Heap region size: 1M[0.017s][info][gc ] Using G1[0.017s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.345s][info][gc,heap,exit ] Heap[0.345s][info][gc,heap,exit ] garbage-first heap total 262144K, used 21504K [0x0000000700000000, 0x0000000800000000)[0.345s][info][gc,heap,exit ] region size 1024K, 18 young (18432K), 0 survivors (0K)[0.345s][info][gc,heap,exit ] Metaspace used 6391K, capacity 6449K, committed 6784K, reserved 1056768K[0.345s][info][gc,heap,exit ] class space used 552K, capacity 571K, committed 640K, reserved 1048576K
Graal[0.019s][info][gc,heap] Heap region size: 1M[0.025s][info][gc ] Using G1[0.025s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.611s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)[0.612s][info][gc,task ] GC(0) Using 6 workers of 10 for evacuation[0.615s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms[0.615s][info][gc,phases ] GC(0) Evacuate Collection Set: 3.1ms[0.615s][info][gc,phases ] GC(0) Post Evacuate Collection Set: 0.2ms[0.615s][info][gc,phases ] GC(0) Other: 0.6ms[0.615s][info][gc,heap ] GC(0) Eden regions: 24->0(150)[0.615s][info][gc,heap ] GC(0) Survivor regions: 0->3(3)[0.615s][info][gc,heap ] GC(0) Old regions: 0->4[0.615s][info][gc,heap ] GC(0) Humongous regions: 5->5[0.615s][info][gc,metaspace ] GC(0) Metaspace: 8327K->8327K(1056768K)[0.615s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 29M->11M(256M) 3.941ms[0.615s][info][gc,cpu ] GC(0) User=0.01s Sys=0.01s Real=0.00sCannot use JVMCI compiler: No JVMCI compiler found[0.616s][info][gc,heap,exit ] Heap[0.616s][info][gc,heap,exit ] garbage-first heap total 262144K, used 17234K [0x0000000700000000, 0x0000000800000000)[0.616s][info][gc,heap,exit ] region size 1024K, 9 young (9216K), 3 survivors (3072K)[0.616s][info][gc,heap,exit ] Metaspace used 8336K, capacity 8498K, committed 8832K, reserved 1056768K[0.616s][info][gc,heap,exit ] class space used 768K, capacity 802K, committed 896K, reserved 1048576K
查看 Graal 在 JDK11 上的编译结果,可以执行下述命令:java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -cp /Users/xxx/IdeaProjects/java_deep_learning/src/main/java/com/msdn/java/javac/escape ScalarTest > out-jvmci.txt
关键词:分析,学习,系列,逃逸