过两年 JVM 可能就要被 GraalVM 干掉了!

大家好,我是老赵

前言

在热心群友的提示下,入了 GraalVM 的坑。本来以为花个三两天应该就能搞定,没想到这一搞就是一个周,终于取得了阶段性的胜利。

编注:此文为运行包精简主题的一篇,其他精简方法请参见文末。

背景如下:

> 最近由于某些原因,需要做一个自带运行环境的程序。由于各种原因,选定了 Java 和 Python 作为备选语言。但是 Java 由于 JRE 的臃肿(100M+)以及 Spring Boot 的日渐臃肿(helloworld 15M),需要在这两方面进行 size 的缩减。

环境

  • 系统:Ubuntu 16.04

  • GraalVM:GraalVM Community 21.3.0 (Based on OpenJDK 11.0.13)

https://www.graalvm.org/downloads/

环境准备

参考:https://www.graalvm.org/docs/getting-started/

  • 关键配置

  • 环境变量设置

export JAVA_HOME=/path/to/<graalvm>
export PATH=/path/to/<graalvm>/bin:$PATH

安装 native-image

gu install native-image

安装 native-image 需要的组件

apt-get install build-essential libz-dev zlib1g-dev

注意:这里有个其他文章都没有提到的坑。ld 需要更新到 2.26+,不然在构建过程中会报告莫名其妙的异常(这里我耗了大半天)。

apt-get install binutils-2.26

另一个坑


最开始我是在一台 aliyun 的机器上实验的,没有注意到内存问题,在实验过程中遇到异常中断。排查 syslog 发现是 OOMKiller。排查发现我的可用内存只有 4G。


后改为在本机机器 VMware 里的 Ubuntu 操作,内存分配到 8G。经观察,native-image 打包过程中会用到 5.2G 左右的内存,所以这里要注意一下。

至此,环境准备完成。

打包

helloworld 的尝试就跳过了,网上一搜一堆。先了解一下打包命令。

2e218ceaf4134b36026ac49c459822e6.png

https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2_A4.pdf

项目依赖

0d94ba354bc9ba4a0b9283f40f99c721.png

主要用到了 solon、solon-api、h2、weed3、logback、slf4j、jlhttp 等包。


首先,通过 Maven 把我的项目 solondemo 打包为可以运行的 jar,确保通过。

java -jar solondemo.jar

可以正常运行并访问。然后把 solondemo.jar 上传到前面准备好的 GraalVM 环境。

首先,需要使用 GraalVM 提供的配置工具,对想要打包的程序的一些静态分析无法分析到的信息进行采集。

如果你确认项目没有使用任何反射、代理等特性,可以省略这一步。执行

java -agentlib:native-image-agent=config-output-dir=./config/ -jar solondemo.jar

执行后,最好能跑一跑 testcase,尽量保证代码覆盖率 100%,避免打包后遇到 classnotfound。

执行完成后,终止执行。


如果需要多次运行采集信息 可以使用如下命令再次执行,工具会自动合并采集结果而不是覆盖。

java -agentlib:native-image-agent=config-merge-dir=./config/ -jar solondemo.jar

执行结束,config 下生成如下 5 个文件:

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

坑又来了

根据网上的 说法,可以通过指定 -H:ConfigurationFileDirectories=./config 的方式来使用前面生成的配置文件。


但是,最后老是会把 -H:ConfigurationFileDirectories= 认为是指定的生成文件名,然而根据文档,-H:Name=xxx 才是指定输出文件名的参数。


注意:这个问题是开始在 CentOS 上操作遇到的,最后我在 Ubuntu 上又尝试用这种方式指定配置文件的时候,它生效了,原因未知。

我采用了另外一种配置方式。把这些文件打包到 jar 包的 META-INF/native-image 目录下。

be9c600a550d813c38f5fbd544ac1786.png

打包命令:

native-image  -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http

这个命令也是经过反复多次尝试最终得出的可用命令,尝试的过程就略去3W字了。

注意:如果你的应用需要对外提供 HTTP 服务,必须加上配置 --enable-http。如果提供 HTTPS 服务,则必须加 --enable-https。否则哪怕运行起来了访问也永远是 500。打包成功,目录下生成 solondem 文件。

执行运行:运行成功了,但是插件没加载。翻阅 solon 源码,发现插件加载的流程大致如下:

404f33f5da9f3ca7a2a6b0e8fbe6e28b.png

通过反复添加日志排查,发现:


在 graalvm native-image 下运行,这里扫描到 META-INF/solon 这个目录的 type,不是 file/jar,而是 resource。

e1f515e10e2c641a8b54066812308fdc.jpeg

所以到了这里自然就无法遍历目录下的文件了。于是我尝试让 resource 类型也走 file 的方式去扫描。

bd255e0b01bcec08f39f1f199aaf539f.png

调研搜索后发现,GraalVM 内部资源管理自己实现了一套 FileSystem,URL 描述符定义为 resource,有一套自定义的 API(由于时间有限,暂未深入研究)。

对本来是目录类型的 resource 使用 File 方式去处理,得到的结果是 file not exists!但是对于确定的文件,是可以正常读取的。

于是我考虑预处理,在 GraalVM 外面就先把能扫描到的文件清单提取出来,通过配置的方式,插件扫描的时候直接返回预置的文件清单。


因为本地执行是可以正常扫描的,所以我在扫描结束的时候,增加一个输出:

3ad9e3df87e0c1509f6b95e8602ff70a.png

然后在配置中添加:

0b0ec7ae023efae67a7674013c27b0c5.jpeg

scan 流程做如下修改

eae88debd7a9b55d58b401bccf353647.png

插件扫描成功并运行。

cdf827344013a3b065ade626d491098e.png

扫描注解也有同样的问题,排查过程与配置文件扫描类似,解决方案已与配置文件扫描的解决方案合并,略去 3万字。

至此,主框架已经可以 run 起来了,但是嵌入式数据库 h2 还在作妖。

调研发现 Github 已经有人提过这个问题:

https://github.com/h2database/h2database/issues/2207

但是我尝试按照他们说的 使用 1.4.199 版本,却仍然各种异常。没办法,下载 h2 源码 加 log 排查吧。

f9741978b59184ef8d8704d4e030981d.png

首先,这里报空指针,那么唯一的可能就是 defaultProvider 为空。分析 defaultProvider 初始化过程:

9a77f45e7b230384737dc6afe057a6b4.png


发现了 Class.forName,以及吃掉了异常:

e.printStackTrace();

打包,再来运行:

Caused by: java.lang.ClassNotFoundException
......
org.h2.store.fs.disk.FilePathDisk

好嘛。native-image 的 agent 居然没有把这个扫出来。手动把这些添加到 reflect-config.json 里面,再打包运行。又报了个别的 class not found。再添加,再打包。

[
........,
{
    "name": "org.h2.store.fs.FilePathDisk",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathMem",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  }  ,
  {
    "name": "org.h2.store.fs.FilePathMemLZF",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathNioMem",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathNioMemLZF",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathNioMapped",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathAsync",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathZip",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathRetryOnInterrupt",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathNio",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.store.fs.FilePathSplit",
    "methods":[{"name":"<init>","parameterTypes":[] }]
  },
  {
    "name": "org.h2.mvstore.db.MVTableEngine",
    "methods":[{"name":"<init>","parameterTypes":[] }],
    "allDeclaredFields":true
  }
]

成了!

82f5bca34f58b6457904eb3de4db7106.png

访问接口、增删改查、静态页面 、日志都 OK 了。

2a319a9e91063fee1e432fd31a23c158.png

81deacffb0ccb33352e384f0f52e4e4f.png

0f3deb03361c1dfa63842b446e96f0d2.png

幸福来得如此突然。

总结

最终可用的打包命令

native-image  -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http

ld 需要升级到 2.26+

root@ubuntu:/home/hx/graalvm/demo3# ld -version


GNU ld (GNU Binutils for Ubuntu) 2.26.1


Copyright (C) 2015 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.

其他注意事项

  • -agentlib:native-image-agent 不一定能检查出所有的反射;

  • GraalVM 有自己的文件系统实现,暂未找到遍历目录的方法;

  • 第三方包不能运行时,大概率是由于反射没有检查到导致的 class not found;

  • 排查第三方包问题时,一定要注意被吃掉的 Exception;

  • native-image 打包时需要 5G+ 内存。

补充

1. native-image后序列化失败问题(比如 JSON.toJSONString(JavaBean))

fastjson1.2.68 版本下在程序启动时增加如下代码:

ParserConfig.getGlobalInstance().setAsmEnable(false);
SerializeConfig.getGlobalInstance().setAsmEnable(false);

2. 反射方法报错

需将反射类手动配置到 reflect-config.json 文件中,也可在编译打包成 jar 时添加配置

-agentlib:native-image-agent=config-output-dir=../META-INF/native-image

后打包,然后 java -jar 或 java -cp 运行起来后,执行对应测试用例后,会自动将反射类信息生成到。

reflect-config.json 文件中(但真的不一定)。配置文件样例:

[
    {
        "name":"com.test.A",
        "allDeclaredFields":true,
        "allPublicFields":true,
        "queryAllPublicMethods":true,
        "methods":[
            {"name":"getA","parameterTypes":[] }, 
            {"name":"getD","parameterTypes":[] }, 
            {"name":"getF","parameterTypes":[] }, 
            {"name":"getI","parameterTypes":[] }, 
            {"name":"getQ","parameterTypes":[] }, 
            {"name":"getR","parameterTypes":[] }, 
            {"name":"getT","parameterTypes":[] }, 
            {"name":"getY","parameterTypes":[] }, 
            {"name":"getU","parameterTypes":[] }, 
            {"name":"getV","parameterTypes":[] }
        ]
    }
]

3. GraalVM 有自己的文件系统实现

暂未找到遍历目录的方法(即上文说说的).

如果你的程序中有涉及 ClassLoad.getResource("com.org") 这样的代码并打算对齐返回的结果以 File 或 jar 文件的方式扫描 com.org 下的所有类文件时会报错。

解决方式如上文所说,手动配置需要扫描的类文件,然后读取该配置(替代 getResource 方式)。

4. 控制 native 化后的二进制程序内存大小(配置参数不多说,一看就明白)

样例:

./solondemo -Xmx16m -Xms16m -XX:MaxDirectMemorySize=8m

上文提到的 solon scan 方法 支持预配置代码已提交到:

https://gitee.com/noear/solon

文中提到的 solondemo 项目,已经提交到:

https://gitee.com/mantouchong/solondemo

其他探索

  • java运行包精简探索(一):https://www.jianshu.com/p/f1da836fb4d6

  • java运行包精简探索(二):https://www.jianshu.com/p/5e3b1913cf3b

链接:jianshu.com/p/f78a016b30fe

 
 

精彩推荐

1.逆向了一款涉黄APP,发现了她们的小秘密...

2.Nacos 2.2 正式发布,太炸裂了!
3.贼 TM 好用的 Java 工具类库
4.还在用 if (obj != null) 判空?那你真的太Low了!
5.我说MySQL单表超过2000w就要分库分表,面试官让我回去等通知!
6.我们公司用了7年的Dubbo架构技术栈(稳的一批),这套架构真绝了!

d54a2e769180f9a8c8d270ccf5c2e658.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值