静态编译实践
文章目录
静态编译1
什么是静态编译
Java静态编译是指将Java程序的字节码在单独的离线阶段编译为汇编代码,其输入为Java的字节码,输出为native image,即二进制native程序。“静态”是相对传统Java程序的动态性而言的,因为传统Java程序是在运行时动态地解释执行和实时编译,所以静态编译需要在执行前就完成程序的编译。
为什么要静态编译
Java经过从解释执行到JIT编译执行的发展演进,虽然其运行时峰值性能在极限情况下已经能够达到比肩C程序的程度,但是在现今云原生的浪潮下,Java与生俱来的冷启动问题越来越突出。小的云原生应用可能在尚未触发JIT编译时就结束退出了,使得JIT编译没有了用武之地,而冷启动的开销却不可避免地影响了云原生应用的响应速度。
Java静态编译技术是一个既兼顾了Java已有生态,又可以彻底解决冷启动问题的技术方案。
Java程序从启动到抵达性能峰值需要经过VM初始化、应用初始化、应用预热3个阶段,会耗费一定的时间,我们可以将这3个阶段的耗时统称为冷启动的开销。
优点
启动性能好,较传统Java应用最高可达到两个数量级的启动性能提升;
占用内存少,一般只需要占用传统Java应用一半的内存;
多语言支持,可以用Java语言编写C/C++程序的库文件。
缺点
不能完全支持Java的动态特性;
不再具有平台无关的特性;
调试、监控等工具生态发生变化,不能使用传统的Java工具。
GraalVM
目前相对成熟的Java静态编译技术方案主要有:
Oracle GraalVM的Substrate VM
主要面向服务器端应用和桌面应用。
华为方舟编译器
面向移动端应用。
关于内存管理2
Serial GC
GraalVM 社区版和商业版都支持
G1 GC
仅GraalVM商业版支持
Heap Dump3
由于jmap工具无法再使用,所以native-image的Heap Dump方式不一样。
GraalVM文档介绍了4种方式:
使用VisualVM创建Heap Dump
启动时使用命令行参数
-XX:+DumpHeapAndExit
dump初始堆发送
SIGUSR1
信号创建Heap Dump使用
org.graalvm.nativeimage.VMRuntime#dumpHeap
API,以编程的方式创建Heap Dump
这里简单介绍最可能用到的第3种:
src/main/resources/META-INF/native-image/native-image.properties
中添加一下参数:1
Args = --enable-monitoring=heapdump
编译后运行应用。
使用一下命令创建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的方式
程序预运行,自动收集4。
下面是一段构建时的日志,我没有任何操作,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
第三方库直接支持。
举例
JSON文件存储在META-INF/native-image/<group.id>/<artifact.id>,以Lettuce配置示例:
|
|
可使用native-image.properties指定构建时的参数
支持GraalVM Native Image的框架
宣称无需修改即可将Spring应用转换为Micronaut应用。但是,不能完全支持Spring,只支持部分。
Spring Boot 3.x
目前项目大多数都是用的Spring体系,考虑到以后的开发和维护,优先选择。
需要升级至jdk17(跟jdk8一样时LTS版本)。
Question
为什么要升级Spring Boot?
Spring Boot的动态特性无法被GraalVM直接支持,native-image-agent虽然可以生成动态特性配置,但是需要预执行程序,且无法保证100%覆盖程序涉及的动态配置。
项目改造
升级jdk
Spring Boot 3.0支持的最小jdk版本为Java17,下载支持Java17的GraalVM即可。这里使用的版本是GraalVM CE 22.3.0。
升级Spring Boot版为3.0.0
javax包改jakarta。
移除不兼容的二方jar包和移除后改造。
Tips
GraalVM可通过SDKMAN下载,本地多jdk版本时方便切换和管理。
|
|
可列出所有jdk版本(包括已经安装的)
|
|
Vendor列可表示对应的版本是哪些公司发布的。
具体信息可查看:https://sdkman.io/jdks
例如:Corretto表示是Amazon发布的,对应的Identifier也已amzn结尾。
未使用sdkman安装的jdk,可以通过下列方式安装,使其在sdkman的管理范围内。
|
|
构建native-image
构建成Docker镜像。
1
mvn -Pnative spring-boot:build-image
直接构建成本地可执行文件
1
mvn -Pnative native:compile
这里使用第2种方式。
|
|
打包时遇到上述错误,原因是:没有设置环境变量以及安装native-image。
设置环境变量
我使用的IDEA内置的Maven,所以环境变量也在IDEA设置了。
安装native-image
可通过GraalVM Updater安装native-image,执行以下命令
|
|
执行失败,报错:
|
|
相关issue:https://github.com/oracle/graal/issues/4048
下载Github的ca证书。
修改$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 }