静态编译1

什么是静态编译

Java静态编译是指将Java程序的字节码在单独的离线阶段编译为汇编代码,其输入为Java的字节码,输出为native image,即二进制native程序。“静态”是相对传统Java程序的动态性而言的,因为传统Java程序是在运行时动态地解释执行和实时编译,所以静态编译需要在执行前就完成程序的编译。

为什么要静态编译

Java经过从解释执行到JIT编译执行的发展演进,虽然其运行时峰值性能在极限情况下已经能够达到比肩C程序的程度,但是在现今云原生的浪潮下,Java与生俱来的冷启动问题越来越突出。小的云原生应用可能在尚未触发JIT编译时就结束退出了,使得JIT编译没有了用武之地,而冷启动的开销却不可避免地影响了云原生应用的响应速度。

Java静态编译技术是一个既兼顾了Java已有生态,又可以彻底解决冷启动问题的技术方案。

Java程序从启动到抵达性能峰值需要经过VM初始化、应用初始化、应用预热3个阶段,会耗费一定的时间,我们可以将这3个阶段的耗时统称为冷启动的开销。

优点

  1. 启动性能好,较传统Java应用最高可达到两个数量级的启动性能提升;

  2. 占用内存少,一般只需要占用传统Java应用一半的内存;

  3. 多语言支持,可以用Java语言编写C/C++程序的库文件。

缺点

  1. 不能完全支持Java的动态特性;

  2. 不再具有平台无关的特性;

  3. 调试、监控等工具生态发生变化,不能使用传统的Java工具。

GraalVM

目前相对成熟的Java静态编译技术方案主要有:

  1. Oracle GraalVM的Substrate VM

    主要面向服务器端应用和桌面应用。

  2. 华为方舟编译器

    面向移动端应用。

关于内存管理2

  1. Serial GC

    GraalVM 社区版和商业版都支持

  2. G1 GC

    仅GraalVM商业版支持

Heap Dump3

由于jmap工具无法再使用,所以native-image的Heap Dump方式不一样。

GraalVM文档介绍了4种方式:

  • 使用VisualVM创建Heap Dump

  • 启动时使用命令行参数-XX:+DumpHeapAndExitdump初始堆

  • 发送SIGUSR1信号创建Heap Dump

  • 使用org.graalvm.nativeimage.VMRuntime#dumpHeap API,以编程的方式创建Heap Dump

这里简单介绍最可能用到的第3种:

  1. src/main/resources/META-INF/native-image/native-image.properties中添加一下参数:

    1
    
    Args = --enable-monitoring=heapdump
    
  2. 编译后运行应用。

  3. 使用一下命令创建Heap dump (放心执行,不会kill应用,kill -SIGUSR1是由应用自定义的,这里被自定义为Heap dump)。

    1
    
    kill -SIGUSR1 <pid>
    

    这会在应用所在目录下生成一个dump文件

封闭性假设

所有运行时的内容必须在编译时可见,并被编译到native image中。但是Java的动态特性原因,需要提供元数据配置补充给静态编译器以满足封闭性。

  • reflect-config.json
  • jni-config.json
  • resource-config.json
  • proxy-config.json
  • serialization-config.json
  • predefined-classes-config.json

文件具体作用和配置方式,详见:https://www.graalvm.org/latest/reference-manual/native-image/metadata/

获取Metadata的方式

  1. 程序预运行,自动收集4

  2. graalvm官方元数据仓库,通过maven插件使用。

    下面是一段构建时的日志,我没有任何操作,maven插件会自动下载元数据仓库的数据并使用。而且会做版本匹配,找不到会使用元数据仓库最新版本

    [INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.4.5]: Configuration directory not found. Trying latest version. [INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.4.5]: Configuration directory is ch.qos.logback/logback-classic/1.4.1 [INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.1]: Configuration directory not found. Trying latest version. [INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.1]: Configuration directory is org.apache.tomcat.embed/tomcat-embed-core/10.0.20 [INFO] [graalvm reachability metadata repository for com.zaxxer:HikariCP:5.0.1]: Configuration directory is com.zaxxer/HikariCP/5.0.1 [INFO] [graalvm reachability metadata repository for com.mysql:mysql-connector-j:8.0.31]: Configuration directory is com.mysql/mysql-connector-j/8.0.31

  3. 第三方库直接支持。

举例

JSON文件存储在META-INF/native-image/<group.id>/<artifact.id>,以Lettuce配置示例:

1
2
3
4
5
6
7
META-INF/
└── native-image
└── io.lettuce
  └── lettuce-core
      └── native-image.properties
      └── proxy-config.json
      └── reflect-config.json

可使用native-image.properties指定构建时的参数

支持GraalVM Native Image的框架

  1. Quarkus

  2. Micronaut

    宣称无需修改即可将Spring应用转换为Micronaut应用。但是,不能完全支持Spring,只支持部分。

  3. Spring Boot 3.x

    目前项目大多数都是用的Spring体系,考虑到以后的开发和维护,优先选择。

    需要升级至jdk17(跟jdk8一样时LTS版本)。

Question

为什么要升级Spring Boot?

Spring Boot的动态特性无法被GraalVM直接支持,native-image-agent虽然可以生成动态特性配置,但是需要预执行程序,且无法保证100%覆盖程序涉及的动态配置。

项目改造

  1. 升级jdk

    Spring Boot 3.0支持的最小jdk版本为Java17,下载支持Java17的GraalVM即可。这里使用的版本是GraalVM CE 22.3.0。

  2. 升级Spring Boot版为3.0.0

  3. javax包改jakarta。

  4. 移除不兼容的二方jar包和移除后改造。

Tips

GraalVM可通过SDKMAN下载,本地多jdk版本时方便切换和管理。

1
sdk list java

可列出所有jdk版本(包括已经安装的)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
================================================================================
 Vendor        | Use | Version      | Dist    | Status     | Identifier
--------------------------------------------------------------------------------
 Corretto      |     | 19           | amzn    |            | 19-amzn
               |     | 19.0.1       | amzn    |            | 19.0.1-amzn
               |     | 17.0.5       | amzn    |            | 17.0.5-amzn
               |     | 17.0.4       | amzn    |            | 17.0.4-amzn
               |     | 11.0.17      | amzn    |            | 11.0.17-amzn
               |     | 11.0.16      | amzn    |            | 11.0.16-amzn
               |     | 8.0.352      | amzn    |            | 8.0.352-amzn
               |     | 8.0.342      | amzn    |            | 8.0.342-amzn
 Gluon         |     | 22.1.0.1.r17 | gln     |            | 22.1.0.1.r17-gln
               |     | 22.1.0.1.r11 | gln     |            | 22.1.0.1.r11-gln
               |     | 22.0.0.3.r17 | gln     |            | 22.0.0.3.r17-gln
               |     | 22.0.0.3.r11 | gln     |            | 22.0.0.3.r11-gln
 GraalVM       |     | 22.3.r19     | grl     |            | 22.3.r19-grl
               |     | 22.3.r17     | grl     |            | 22.3.r17-grl
               |     | 22.3.r11     | grl     |            | 22.3.r11-grl
               |     | 22.2.r17     | grl     |            | 22.2.r17-grl
......               

Vendor列可表示对应的版本是哪些公司发布的。

具体信息可查看:https://sdkman.io/jdks

例如:Corretto表示是Amazon发布的,对应的Identifier也已amzn结尾。

未使用sdkman安装的jdk,可以通过下列方式安装,使其在sdkman的管理范围内。

1
sdk install java 17-zulu /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home

构建native-image

  1. 构建成Docker镜像。

    1
    
    mvn -Pnative spring-boot:build-image
    
  2. 直接构建成本地可执行文件

    1
    
    mvn -Pnative native:compile
    

这里使用第2种方式。

1
2
GraalVM native-image is missing from your system.
Make sure that GRAALVM_HOME environment variable is present.

打包时遇到上述错误,原因是:没有设置环境变量以及安装native-image。

设置环境变量

我使用的IDEA内置的Maven,所以环境变量也在IDEA设置了。

我裂开了

安装native-image

可通过GraalVM Updater安装native-image,执行以下命令

1
gu install native-image

执行失败,报错:

1
I/O error occurred: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

相关issue:https://github.com/oracle/graal/issues/4048

  1. 下载Github的ca证书。

    我裂开了

    我裂开了

    我裂开了

  2. 修改$GRAALVM_HOME/lib/security/cacerts

    1
    
    keytool -importcert -alias cert01 -keystore "$GRAALVM_HOME/lib/security/cacerts" -file "/Users/liuqiang/Desktop/github.com.cer" -storepass "changeit" -noprompt
    

    $GRAALVM_HOME替换为自己安装的GraalVM路径。keytool文档:https://docs.oracle.com/en/java/javase/17/docs/specs/man/keytool.html#importing-a-certificate-for-the-ca

    可通过以下命令查看路径:

    1
    
    /usr/libexec/java_home -V
    

解决方案参考:https://stackoverflow.com/questions/71035433/graalvm-windows-native-image-installation-problem

也可以github下载后本地安装,安装方式见:https://www.graalvm.org/latest/reference-manual/graalvm-updater/#install-cmponents-from-local-collection

MyBatis错误

mybatis-spring-boot-starter版本3.0.0, 截止至2022.12.02,3.0.1的快照版仍然无法解决问题,所以未使用。

  • Error creating logger for logger org.mybatis.spring.mapper.ClassPathMapperScanner.

    解决办法,src/main/resources/META-INF/native-image目录下添加reflect-config.json

    1
    2
    3
    4
    5
    6
    
    [
      {
        "name":"org.apache.ibatis.logging.slf4j.Slf4jImpl",
        "methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
      }
    ]
    

    或者配置类上添加

    1
    
    @RegisterReflectionForBinding(Slf4jImpl.class)
    
  • dataSource or dataSourceClassName or jdbcUrl is required.

    这个错是由HikariCP打印出来的,但却是Mybatis的MapperScanner引起的。现象与此issue(https://github.com/mybatis/spring/issues/30)相同。

    那么, 只需要绕过MapperScanner即可。参考Mybatis的Registering a mapper:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    @Configuration
    public class MyBatisConfig {
      @Bean
      public MapperFactoryBean<UserMapper> userMapper() throws Exception {
        MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
        factoryBean.setSqlSessionFactory(sqlSessionFactory());
        return factoryBean;
      }
    }
    

    使用上述方式即可,单一数据源不需要配置MapperScanner。所以没有此问题,也不需要上述配置。

  • SqlSessionFactory无法实例化

    这是反射相关问题,所以reflect-config.json配置下列内容即可解决:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    [
      {
        "name":"org.apache.ibatis.javassist.util.proxy.ProxyFactory"
      },
      {
        "name":"org.apache.ibatis.scripting.xmltags.XMLLanguageDriver",
        "methods":[{"name":"<init>","parameterTypes":[] }]
      },
      {
        "name":"org.apache.ibatis.scripting.defaults.RawLanguageDriver",
        "methods":[{"name":"<init>","parameterTypes":[] }]
      }
    ]
    
  • Proxy class defined by interfaces [interface io.github.lqiang.demo.mapper.UserMapper] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.

    动态代理的问题,给对应Mapper添加配置即可。

    src/main/resources/META-INF/native-image目录下添加proxy-config.json

    1
    2
    3
    4
    5
    
    [
      {
        "interfaces":["io.github.lqiang.demo.mapper.UserMapper"]
      }
    ]
    
  • 找不到实体类构造函数

    同样是反射的问题,给对应的类加上反射配置即可。

    1
    2
    3
    4
    5
    
    {
        "name":"io.github.lqiang.demo.entity.User",
        "allPublicConstructors": true,
        "allPublicMethods": true
    }
    

Tips

所有上面的反射和动态代理等配置都可以通过Tracing Agent生成。

可参考Spring Boot文档:Using the Tracing Agent