通过实战探究 GraalVM 静态编译

Posted engchina

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过实战探究 GraalVM 静态编译相关的知识,希望对你有一定的参考价值。

通过实战探究 GraalVM 静态编译

GraalVM 是什么

GraalVM 是 Oracle 推出的基于 Java 开发的开源高性能多语言运行时平台。

GraalVM 分为免费的社区版(Community Edition,CE)和收费的企业版(Enterprise Edition,EE),两者的基本功能是相同的,但是 EE 版的性能和安全性更高,并为用户提供不间断的 24 小时支持服务。Oracle 云服务的客户可以免费获得 EE 版。

什么是 Java 静态编译

Java 静态编译时指将 Java 程序的字节码在单独的离线阶段编译为汇编代码,其输入为 Java 的字节码,输出为 native image,即二进制 native 程序。

静态编译的基本原则时封闭性假设(closed world assumption),要求编译器在编译时必须掌握运行时所需的全部信息。

GraalVM 静态编译优点

GraalVM 实现了 Java 静态编译的编译器、编译框架和运行时等一整套完整的工具链。GraalVM 的 Java 静态编译器就是 GraalVM 底层的 GraalVM JIT Compiler,这意味着 GraalVM 统一了 JIT 和 AOT 编译器。

静态编译框架和运行时时由 Substrate VM 子项目实现的。

GraalVM 的静态编译方案的基本思路是由用户指定编译入口(比如 main 函数),然后编译框架从入口开始静态分析程序的可达范围,编译器将分析出的可达函数和 Substrate VM 中的运行时支持代码一起编译为一个被称为 native image 的二进制本地代码文件。

根据用户的参数设置,这个本地代码文件既可以时 ELF 可执行文件,也可以是动态共享库文件。

GraalVM 通过静态分析的指向分析(points-to-analysis)、控制流分析(control flow anylysis)以及调用图分析(call graph analysis)等技术找到可达的代码范围。

GraalVM 静态编译方案还实现了多种运行时优化,典型的有对 Java 静态初始化过程的优化。

GraalVM 静态编译缺点

静态分析是资源密集型计算,需要消耗大量的内存、CPU 和时间。

静态分析对反射的分析能力非常有限。

静态编译后程序的运行时性能低于传统 Java 经过 JIT 编译后的峰值性能。

Substrate VM 是什么

Substrate VM 提供了将 Java 程序静态编译为本地代码的编译工具链,包括了编译框架、静态分析工具、C++ 支持框架及运行时支持等。

但是 Substrate VM 中并没有编译器和链接器,因为其编译器是 GraalVM 编译器,而链接器则使用了 GCC(在 Linux 系统上)。

预执行目标应用程序

Java 语言的动态特性违反了静态编译的封闭性假设。GraalVM 允许通过配置的形式将缺失的信息补充给静态编译器以满足封闭性,为此 GraalVM 设计了 reflect-config.json、jni-config.json、resource-config.json、proxy-config.json、serialization-config.json 和 predefined-classes-config.json 这 6 个 json 格式的配置文件,分别用于向静态编译提供反射目标信息、JNI 回调目标信息、资源文件信息、动态代理目标接口信息、序列化信息和提前定义的动态类信息。

预执行只需在目标应用程序原本的启动命令基础上,添加如下参数启动 Agent 即可。

java -agentlib:native-image-agent=config-output-dir=$CONFIG_ROOT/META-INF/native-image AppMain

我们基于一个 SpringBoot 3的项目,实际尝试一下,

启动 SpringBoot3 项目,访问其中的一个 API,然后停止这个 SpringBoot3 项目,我们可以看到 reflect-config.json、jni-config.json、resource-config.json、proxy-config.json、serialization-config.json 和 predefined-classes-config.json 这 6 个 json 格式的配置文件被创建出来了。

在编译时静态编译框架会自动从 classpath 的 META-INF/native-image 目录结构中识别出配置文件,此外,用户可以在启动 native-image 时传入多个参数以额外指定配置文件的位置。

  • -H:ConfigurationFileDirectories=:指定配置文件的直接目录,多个项目之间用逗号分隔。在该目录中按默认方式的命名的 json 配置文件都可以被自动识别。

  • -H:ConfigurationResourceRoots=:指定配置资源的根路径,多个项目之间用逗号分隔。配置文件不仅可以被当作外部文件读取,也可以被当作 resource 资源读取。这种方式适用于读取存放在 jar 文件中的配置文件。

  • -H:XXXConfigurationFiles=:指定某一种类型的配置文件,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。

  • -H:XXXConfigurationResources=:指定某一种类型的配置资源的路径,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。

静态编译目标应用流程

应用静态编译的基本流程是,首先获取 GraalVM JDK,然后获取目标应用程序及所需的依赖库,接下来 GraalVM JDK 预执行目标应用程序获取所有的动态特性配置,最后以目标应用程序、依赖库和动态特性配置作为输入,通过 GraalVM JDK 中的静态编译启动器 native-image 启动静态编译框架,编译出二进制本地可执行应用程序文件。

GraalVM 可以将 jar 包或者未打包的 class 文件编译为 ELF(Executable and Linkable Format)格式的二进制文件或者动态共享库文件。

目前 GraalVM 支持通过 4 种方式调用静态编译启动器,分别是命令行模式、配置文件模式、Maven 插件模式和 Gradle 插件模式。其中命令行模式是基础,其他 3 种都是对命令行模式的包装。

命令行模式

执行静态编译的基本命令格式是:

native-image -cp $CP $OPTS [app.Main]

或者

native-image $OPTS -jar [app.jar]

$OPTS 是编译时设置的选项,从使用的角度可以分为启动器选项、编译器选项和运行时选项三大项。

接下来详细说明几个常用选项。

1)启动器选项用于控制启动器行为,或通过启动器传递给 Substrate VM。

  1. -cp、–classpath、-jar、–version:虽然 native-mage 启动器并非 Java 程序,但是这些选项与 Java 的同名选项含义相同。
  2. –debug-attach[=<port>]:在指定端口开启远程调试,默认端口时 8000。
  3. –dry-run:启动器仅做参数解析包装工作,然后输入最终实际启动静态编译框架的所有参数,但不真正启动静态编译框架。其主要用于调试。
  4. –help、–help-extra、–expert-options-all:打印输出选项的帮助信息。
  5. 编译器参数:编译器参数用于控制静态编译器的行为,少部分常用选项以 -- 为前缀,可以通过 native-image --help 查看;更多的是以 -H: 为前缀(目前的 GraalVM EE Java 17 中有 887 个)的高级选项,这些选项可以通过执行 native-image --expert-options-all | grep "\\-H:" 查看。
  6. -J<Flag>:设置 native-image 编译框架本身的 JVM 参数。
  7. –no-fallback:从不进入 fallback 模式。当 Substate VM 发现使用了未被配置的动态特性时会默认回退到 JVM 模式。本选项会关闭回退行为,总是执行静态编译。
  8. –report-unsupported-elements-at-runtime:当发现应用程序中使用了静态编译不支持的特性时不立即报告并终止编译,而是继续完成编译,等到运行时第一次执行到不支持的特性再报告。
  9. –allow-incomplete-classpath:允许不完全的 classpath。
  10. –initialize-at-run-time:将指定的单个类或包中的所有类的初始化推迟到运行时。
  11. –initialize-at-build-time:将指定的单个类或包中的所有类的初始化提前到编译时。
  12. –shared:将程序编译为共享库文件,不加此项默认将应用程序编译为可执行文件。编译共享库文件时需用 CLibrary 的注解 @CEntryPoint 标识共享库暴露的 API 作为编译入口。
  13. -H:Name:指定编译产生的可执行文件的名字。如不指定,则默认以主函数所在类的全部小写的类全名(full qualified name)为文件名。
  14. -H:-DeleteLocalSymbols:禁止删除本地符号,本参数默认设置为打开,即会删除本地符号。为了减少编译文件的大小,编译器会将程序中的本地符号删除,但是缺少符号信息会在调试时难以定位代码。因此,如果有调试需求,可以关闭此选项。
  15. -H:+PreserveFramePointer:保留栈帧指针信息,本参数默认为关闭。同样是为了减少编译文件的大小,默认不会保留栈帧指针,这会导致在调试时无法显示调用栈名,而只能看到问号。因此,如有调试需求,可以将此参数设置为打开。
  16. -H:+ReportExceptionStackTraces:打印编译时异常的调用栈,本参数默认为关闭。打开后就可以在静态编译出错时输出完整的异常调用栈信息,帮助发现异常原因以便修复。

2)运行时参数:运行时参数用于控制可执行程序的运行时表现,以 -R: 开头(目前的 GraalVM EE Java 17 中有 671 个),这些选项可以通过执行 native-image --expert-options-all | grep "\\-R:" 查看。

配置文件模式

当静态编译使用编译参数较多时,GraalVM 官方推荐使用配置文件管理。

目前配置文件支持用户自行配置 3 个属性。

  • Args:设置各项参数。不同参数用空格分隔,换行用 \\

  • JavaArgs:设置静态编译框架本身的 JVM 参数,等同于命令行模式的 -J<Flag>

  • ImageName:设置编译生成的文件名,等同于命令行模式的 -H:Name。

配置文件的默认保存路径是静态编译时 classpath 下的 META-INF/native-image/native-image.properties。Substrate VM 会从 classpath 的文件目录结构或 classpath 上的 jar包中按上述路径寻找有效的配置文件。

Maven 模式

GraalVM 也支持通过 Maven 插件启动静态编译,示例配置信息如下,

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>$native.maven.plugin.version</version>
    <extensions>true</extensions>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>build</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
    <configuration>
        <!-- Set this to true if you need to switch this off -->
        <skip>false</skip>
        <!-- The output name for the executable sh exe -->
        <imageName>$artifactId</imageName>
        <mainClass>$app.main.class</mainClass>
        <buildArgs>
            <!-- With Enterprise you can use the G1GC -->
            <!--buildArg>- -gc=G1</buildArg-->
            <buildArg>
                -Dgraal.CompilationFailureAction=Diagnose
                -Dgraal.ShowConfiguration=info
                -Dspring.native.verbose=true
                -Dspring.native.verify=false
                --verbose
                --allow-incomplete-classpath
                --report-unsupported-elements-at-runtime
                --diagnostics-mode
                --no-fallback
                -H:+ReportExceptionStackTraces
                -H:+PrintAOTCompilation
                -H:+PrintClassInitialization
                -H:+PrintFeatures
                -H:+PrintHeapHistogram
                -H:+PrintImageElementSizes
                -H:+PrintImageHeapPartitionSizes
                -H:+PrintJNIMethods
                -H:+PrintUniverse
                -H:+PrintMethodHistogram
                -H:+PrintRuntimeCompileMethods
                -H:Log=registerResource:3
                -H:+DynamicProxyTracing
                -H:+LogVerbose
                -H:+ProfileDeoptimization
                -H:TraceClassInitialization=true
                -H:+AddAllCharsets
                -H:+JNI
                -H:+DashboardAll
                -H:+DashboardPretty
                <!--
                    -H:+ReflectionPluginTracing
                -->
                <!--
                    -H:+TraceLoggingFeature
                -->
                <!--  
                    -H:+TraceLocalizationFeature
                    -H:+TraceSecurityServices
                    -H:+TraceServiceLoaderFeature
                    -H:+TraceVMOperations
                -->
                -H:DeadlockWatchdogInterval=10
                -H:+DeadlockWatchdogExitOnTimeout
                -H:+PrintAnalysisCallTree
                -H:+PrintAnalysisStatistics
                -H:+PrintCompilation
            </buildArg>
        </buildArgs>
    </configuration>
</plugin>

静态编译的信息在 <configuration> 项中配置。

  • <skip>:控制是否执行静态编译,true 表示不执行,false 表示执行。

  • <imageName>:配置编译的文件名。

  • <mainClass>:设置编译的入口主类名。

  • <buildArgs>:设置编译参数,多个参数之间用空格分隔。

Gradle 模式

静态编译 HelloWorld

创建一个 HelloWorld.java 文件,代码如下,

public class HelloWorld 
    public static void main(String[] args) 
        System.out.println("Hello World!");
    

通过执行下列 2 行命令即可编译得到 HelloWorld 程序的二进制可执行文件 HelloWorld,

javac HelloWorld.java
native-image -cp ./ HelloWorld

运行 helloworld 程序,

./helloworld

--- output
Hello World!
---

静态编译 SpringBoot3 项目

创建一个 SpringBoot3 项目,依赖里面勾选 “GraalVM Native Support”,

修改启动类的代码如下,

package com.oracle.springboot3nativeimage;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@SpringBootApplication
public class NativeApplication 

    @GetMapping("/")
    public String sayHello() 
        log.info("### In NativeApplication.sayHello() ###");
        return "Hello World!";
    

    public static void main(String[] args) 
        SpringApplication.run(NativeApplication.class, args);
    


通过执行下列命令即可编译得到 SpringBoot3 程序的二进制可执行文件 nativeapplication,

./mvnw -Pnative native:compile

运行 nativeapplication 程序,

./target/nativeapplication

访问 localhost:8188

curl localhost:8188; echo

--- output
Hello World!
---

通过执行下列命令即可构建得到包含 SpringBoot3 程序的二进制可执行文件的容器镜像,

./mvnw -Pnative spring-boot:build-image

从输出的日志中,我们能查看到整个构建工作中执行了 native-image 命令,

(略)
Executing native-image -H:+StaticExecutableWithDynamicLibC -H:Name=/layers/paketo-buildpacks_native-image/native-image/com.oracle.springboot3nativeimage.NativeApplication -cp /workspace:/workspace/BOOT-INF/classes:/workspace/BOOT-INF/lib/logback-classic-1.4.6.jar:/workspace/BOOT-INF/lib/logback-core-1.4.6.jar:/workspace/BOOT-INF/lib/log4j-to-slf4j-2.19.0.jar:/workspace/BOOT-INF/lib/log4j-api-2.19.0.jar:/workspace/BOOT-INF/lib/jul-to-slf4j-2.0.7.jar:/workspace/BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar:/workspace/BOOT-INF/lib/snakeyaml-1.33.jar:/workspace/BOOT-INF/lib/jackson-databind-2.14.2.jar:/workspace/BOOT-INF/lib/jackson-annotations-2.14.2.jar:/workspace/BOOT-INF/lib/jackson-core-2.14.2.jar:/workspace/BOOT-INF/lib/jackson-datatype-jdk8-2.14.2.jar:/workspace/BOOT-INF/lib/jackson-datatype-jsr310-2.14.2.jar:/workspace/BOOT-INF/lib/jackson-module-parameter-names-2.14.2.jar:/workspace/BOOT-INF/lib/tomcat-embed-core-10.1.7.jar:/workspace/BOOT-INF/lib/tomcat-embed-el-10.1.7.jar:/workspace/BOOT-INF/lib/tomcat-embed-websocket-10.1.7.jar:/workspace/BOOT-INF/lib/spring-web-6.0.7.jar:/workspace/BOOT-INF/lib/spring-beans-6.0.7.jar:/workspace/BOOT-INF/lib/micrometer-observation-1.10.5.jar:/workspace/BOOT-INF/lib/micrometer-commons-1.10.5.jar:/workspace/BOOT-INF/lib/spring-webmvc-6.0.7.jar:/workspace/BOOT-INF/lib/spring-aop-6.0.7.jar:/workspace/BOOT-INF/lib/spring-context-6.0.7.jar:/workspace/BOOT-INF/lib/spring-expression-6.0.7.jar:/workspace/BOOT-INF/lib/spring-boot-3.0.5.jar:/workspace/BOOT-INF/lib/spring-boot-autoconfigure-3.0.5.jar:/workspace/BOOT-INF/lib/slf4j-api-2.0.7.jar:/workspace/BOOT-INF/lib/spring-core-6.0.7.jar:/workspace/BOOT-INF/lib/spring-jcl-6.0.7.jar:/workspace/BOOT-INF/lib/spring-boot-jarmode-layertools-3.0.5.jar com.oracle.springboot3nativeimage.NativeApplication
(略)

运行容器镜像,

docker run --rm -it -p 8188:8188 nativeapplication:0.0.1


访问 localhost:8188

curl localhost:8188; echo

--- output
Hello World!
---

未完待续!

Java技术指南「编译器专题」深入分析探究“静态编译器”(JAVAIDEAECJ编译器)

技术分析

  • 大家都知道Eclipse已经实现了自己的编译器,命名为 Eclipse编译器for Java (ECJ)。
  • IDEA所支持的编译器,也有几种:javac(Java原生编译器)、ECJ(支持使用Eclipse编译器)、ACJ编译器(不太清楚),其中默认使用的是Javac,同时也推荐使用Javac。

有兴趣可以看看ECJ编译器的相关使用以及独立使用ECJ

大家的误解

首先,很多小伙伴们都跟我说过Javac和JIT还有AOT它们都是什么有啥区别啊?其实无论是ECJ之类的Java源码编译器运行的时候,也都是就是静态编译(前端编译器),而不是JVM里的JIT(主要面向与优化!),此处之前的文章介绍过JIT和AOT编译器,所以此处不做过多赘述!

主流的使用方式

  • “主流”Java系统的做法——javac + HotSpot VM的组合就是如此。

  • 运行时虚方法內联(virtual method inlining)就是这种例子。这样就可以跨越Class边界做优化,跟C/C++程序的LTO(link-time optimization)一样,不过C/C++程序真在运行时做LTO的很少,这方面反而是Java“更胜一筹”…呃,C/C++写的一个动态链接库通常也有大量代码可以放在一起优化,对LTO的需求本来就远没有Java高。

静态编译阶段

首先要确定概念:“编译期”肯定是指诸如Javac、ECJ之类的Java源码编译器运行的时候,也就是静态编译;而不是JVM里的JIT编译器运行的时候,也就是动态编译!

动态编译器之优化!

WALA为例,有简单的逃逸分析实现:

例如,方法内逃逸分析:TrivialMethodEscape

javac优化能力分析

  • 你肯定会有一个疑问?那为啥没见到啥现成的产品在编译器时做逃逸分析和相关优化,或者为啥javac不做这种优化?

  • 回答:目前Javac几乎啥优化都不做,优化的操作和能力都交接了JVM(动态编译器)实现了,并非是技术原因(技术无法实现?),主要是Sun / Oracle的公司压根就没有考虑在Javac的时候进行代码优化操作。

不过即使是这样,仍然有现成的产品做这种事情啊,只不过是针对Android,大家可以参考DexGuard

  • 但是也还是有一些值得注意的优化尚未得到支持:

    • 例如将一些常量值提取到循环之外!
    • 以及一些相关的逃逸分析技术考虑,具体可以参考其相关官方文档!
  • Java也有静态编译优化技术,例如,[Excelsior JET]http://www.tucows.com/preview/371869/Excelsior-JET-For-Windows)比HotSpot VM早得多就实现了逃逸分析及相关优化,而且是静态编译时做的而不是运行时(JIT)做的。

  • Excelsior JET是一个AOT(Ahead-of-Time)编译器和运行时系统。

技术难点在哪里?

  • 主要就是Java的分离编译(separate compilation)和动态类加载(dynamic class loading)/动态链接(dynamic linking)。

  • 不知道运行时会加载并链接上什么代码,但是具体原因不仅仅是“反射”“运行时字节码增强(runtime bytecode instrumentation)”。

Java的标准做法是把每个引用类型编译为一个单独的Class文件,这些Class文件可以单独的被重新编译,在运行时可以单独的被动态加载。例如说:

// Foo.java
public class Foo {
  public void greet(Bar b) {
    System.out.println("Greetings, " + b.toString());
  }
}
// Bar.java
public class Bar {
  public String toString() {
    return "Bar 0x" + hashCode();
  }
}
  • 这两个Java源码文件可以单独编译,也可以单独重编译,生成出Foo.class与Bar.class两个Class文件。它们在运行时可以单独被JVM加载,而且每个ClassLoader实例都可以加载一次所以同一个Class文件可能会在同一个JVM实例里被加载多次并被看作不同的Class。

  • 当在静态编译Foo.java时,无法假设运行时真的遇到的Bar实现跟现在看到的Bar.java还是一样,所以不能跨类型边界(编译后变成Class文件边界)做优化。

  • 这种问题其实跟C/C++程序通常无法跨越动态链接库的边界做优化一样,只不过一般的Class文件内包含的代码远比不上一个native的动态链接库,但是受的优化限制却一样,使得对Java程序的静态分析与优化的收益非常受限。

  • 外加Java的面向对象特性带来的一些“副作用”:

    • 一个风格良好的面向对象程序通常会有大量很小的方法,方法之间的调用非常多,而且很可能是对虚方法的调用(invokevirtual),Java的非私有实例方法默认是虚方法。

    • 一个类与它的派生类必然不会在同一个Class文件里,这样即便一个类的A方法调用该类的B方法,也未必能做有效的分析和优化。
例如:
public class Foo {
  public Object foo() {
    return bar(new Object());
  }
  public Object bar(Object o) {
    return null;
  }
}

对这个类,我们能不能把Foo.foo()静态优化,內联Foo.bar()并消除掉无用的new Object(),最好优化成return null呢?

public class Bar extends Foo {
  public Object bar(Object o) {
    return o;
  }
}

被加载进来。假如有:

Foo o = new Bar();
o.foo(); // not null
  • 结合起来看,Java有很多小方法、很多虚方法调用、难以静态分析。

  • 而逃逸分析恰恰需要在比较大块的代码上工作才比较有效:JIT编译器要能够看到更多的代码,以便更准确的判断对象有没有逃逸。

  • 只保守的在小块代码上分析的话,很多时候都只能得到“对象逃逸了”的判断,就没啥效果了。

这些特性使得对Java程序做高质量的静态分析变得异常困难:

  • 运行时各种类都加载进来之后再激进的假设那就是当前已经加载的类就代表了“整个程序”,以“closed world”假设做激进优化,但留下“逃生门在遇到与现有假设冲突的新的类加载时抛弃优化,退回到安全的非优化状态。

  • 要么可以抛弃Java的分离编译+动态加载特性,简化原始问题 ,这样就什么静态分析和优化都能做了。上面提到的DexGuard、Excelsior JET都走这个路线。

Excelsior JET的实现优化的标准和条件

  • 那样标榜自己实现了标准Java,但又做很多静态编译优化,这又是怎么回事?

  • 其实Java标准只是说要整个系统看起来维持动态类加载的表象,并没有说所有程序都一定要用动态类加载。

  • 假如有一个Java应用,它不关心通过动态链接带来的灵活性,而是在开发时就可以保证所有用到的类全都能静态准备好,而且不在运行时“灵活”的实用ClassLoader,那它完全可以找一个能对这种场景优化的Java系统来执行它。

  • Excelsior JET就是针对这样的场景优化的。用户在使用JET把Java程序编译成native code时,可以指定编译模式是“我声明我的应用肯定不会用某些动态特性”,JET就会相应的尝试激进的做静态全局编译优化。

动态类加载的Java程序怎么办?

跟Excelsior JET类似的系统还有一些,最出名的可能是GCJ,不过我觉得它没Excelsior做得完善。根据GCJ的todo列表,很明显它还没实现逃逸分析和相关优化。

国内的话,复旦大学有过一个基于Open64的Java静态编译器项目,叫做Opencj。

请参考论文:Opencj: A research Java static compiler based on Open64

反射和运行时字节码增强它们不是主要问题。

反射

怎样算是可以修改类的结构信息?

  • 修改类的基类,或修改类实现的接口
  • 添加或删除成员(成员方法或字段都算)
  • 修改现有成员的类型(例如修改成员变量的声明类型,或者修改成员方法的signature之类)

参数无法静态确定的反射调用是没办法靠静态分析得知调用目标的。

运行时字节码增强
  • Java程序运行的过程中修改程序逻辑的能力,从Java提供这一功能的方法就可以一窥其目的:这个能力主要不是给普通Java程序使用,而是给profiler / debugger用的。

  • Java运行时字节码增强,要么得用Java agent来使用[java.lang.instrument]包里的功能,要么得用JVMTI接口写C/C++代码实现个JVM agent;普通的、不使用agent的Java程序是用不了这种功能的。讨论Java程序是否能在某场景下优化的话题,一般没必要考虑对运行时字节码增强的支持。

即便要支持,主流JVM通过JIT编译器可以重复多次优化编译代码,优化的代码可以被抛弃退回到非优化形式执行,从而既可以激进的做优化、又可以安全的支持这些动态功能;像Excelsior JET这种主要以AOT方式编译Java代码的,为了能提供完善的Java支持还是可选在运行时带有JIT编译器。

  • Javassist,这就是典型的运行时Java字节码增强的应用。

  • ASM库也是如此。

以上是关于通过实战探究 GraalVM 静态编译的主要内容,如果未能解决你的问题,请参考以下文章

Java技术指南「编译器专题」深入分析探究“静态编译器”(JAVAIDEAECJ编译器)

Java技术专题「编译器专题」深入分析探究“静态编译器”(JAVAIDEAECJ编译器)是否可以实现代码优化?

Windows下使用Graalvm将Springboot应用编译成exe大大提高启动和运行效率

基于GraalVM的PHP JIT实现性能优于原生方案

Spring Native

Spring Native