最近v2ex上java的热门话题是java太占内存了balabala,就挺感慨,java当年从一个在家电之类的小型嵌入设备语言,发展到今天的“企业级”应用语言,确实变成吃内存大户了,当然如今的环境下,多个百十来M的内存,和jvm、spring全家桶带给我们的开发便利与稳定性相比,就不值一提了。几篇帖子的测试表明,java裸跑一个hello world都要消耗30m的内存,在一些极端场景(比如跑在内存极其苛刻的物联网设备上),还是有些无法接受。
所以,本文介绍了基于graalvm打native包的方式,为您提供一种在需要轻量级环境下运行java程序的选择。
native优缺点
native包的优点:启动快、占用内存少;不用java环境(windows下直接双击exe运行,linux下敲./xxx名运行)
native包的缺点:没有JIT的优化,性能通常会差一些;不能一处编译到处运行了,需要打多个平台的native包;反射等代码处理起来比较麻烦
环境准备
windows平台
1)安装graalvm jdk
https://www.graalvm.org/downloads/# 下载windows版的graalvm jdk,
然后解压,像普通jdk一样配置环境变量:
这里把GRAALVM_HOME也配上,不然后面用命令行编译的话会报找不到GRAALVM_HOME
2)安装VisualStudio
要编译成.exe文件,必须做这一步,否则后面会报错"native-image-xxx.args xxx returned non-zero result"
去这个地址下载
然后安装时勾选“使用C++的桌面开发”,子选项里把“MsVC v143 - vS 2022C++ x64/x86生成工...”、“Windows 10 SDK (10.0.20348.0)”这俩勾上,如果你是win11就勾11的。
然后一路下一步,安装完后重启电脑,我们的环境就准备完成了。
linux平台
linux下就简单多了, https://www.graalvm.org/downloads/# 下载linux版的graalvm jdk,解压后 vi ~/.bashrc添加如下环境变量
export JAVA_HOME=/mydata/java/graalvm-jdk-21
export GRAALVM_HOME=/mydata/java/graalvm-jdk-21/
export JAVA_BIN=$JAVA_HOME/bin
export JAVA_LIB=$JAVA_HOME/lib
export CLASSPATH=.:$JAVA_LIB/tools.jar:$JAVA_LIB/dt.jar
export PATH=$JAVA_BIN:/mydata/java/maven/bin:$PATH
source ~/.bashrc 让环境变量生效就ok了。
(为了方便管理,我是单独建了一个graalvm来搞)
编写一个helloworld
新建一个maven项目,修改下pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>nativedemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<packaging>jar</packaging>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jdk.version>21</jdk.version>
<release.version>21</release.version>
<mainClass>t1.MemTest</mainClass>
<native.maven.plugin.version>0.10.2</native.maven.plugin.version>
</properties>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
<build>
<plugins>
<!-- 打普通jar包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${mainClass}</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<!-- 打native包 -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<configuration>
<mainClass>${mainClass}</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<id>graalvm-native-build-tools-snapshots</id>
<name>GraalVM native-build-tools Snapshots</name>
<url>https://raw.githubusercontent.com/graalvm/native-build-tools/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
再添加一个测试类
package t1;
public class MemTest {
public static void main(String[] args) throws InterruptedException {
//写一段测耗时的代码
System.out.println("Starting memory test");
long startTime = System.currentTimeMillis();
double sum = 0;
for (long i = 0; i < 10000000000L; i++) {
sum += Math.sqrt(i);
}
long endTime = System.currentTimeMillis();
System.out.println(sum + "\nMemory test completed in " + (endTime - startTime) + " ms");
//sleep主线程,方便测内存
Thread.sleep(Long.MAX_VALUE);
}
}
好了,idea里maven reload一下,就可以点这两个按钮来分别编译jar包或native包了
也可以直接运行如下命令编译jar包或native包
#jar
mvn package
#native
mvn org.graalvm.buildtools:native-maven-plugin:build
两个命令没有前后关系,可以独立运行
打包完成后,target目录下得到如下产物:
native只有6m的可独立运行文件,跑起来占用大约6-8m,效果还是比较好的。
但是运行耗时会稍微变长一点:
如果需要在本地包运行时添加jvm参数,或者传参到main方法的args中,可以这样:
public static void main(String[] args) throws InterruptedException {
System.out.println("Starting memory test");
for (String arg : args) {
System.out.println(arg);
}
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("Xmx "+(maxMemory / (1024 * 1024)) +"m");
...
nativedemo.exe -Xmx1g hello
#打印
#hello
#Xmx 1024 m
解决反射
我们把demo改一下,用反射来加载一个方法并调用,按上面的配置直接打包并执行 nativedemo.exe t2.Service1 的话会报错 ClassNotFoundException:
package t1;
import java.lang.reflect.Method;
public class MemTest {
public static void main(String[] args) throws Exception {
System.out.println("Starting memory test 2");
Class<?> clazz = Class.forName(args[0]);
Object instance = clazz.getConstructor().newInstance();
Method method = clazz.getMethod("hello", String.class);
String res = (String) method.invoke(instance, "world");
System.out.println(res);
Thread.sleep(Long.MAX_VALUE);
}
}
package t2;
public class Service1 {
public String hello(String name) {
return "hello " + name;
}
}
此时,我们需要新建一个文件src/main/resources/META-INF/native-image/reflect-config.json,并在其中指明我们需要反射操作的类:
[
{
"name" : "t2.Service1",
"allDeclaredConstructors" : true,
"allPublicMethods" : true
}
]
整体文件结构如下:
再次编译(有时候需要先clean、package一下),运行nativedemo.exe t2.Service1 就能成功反射到了。
另,获取native包所在路径时,用ClassLoader行不通,可以用如下代码:
File currentFile = new File(MemTest.class.getProtectionDomain().getCodeSource().getLocation().toURI());
String currentDir = currentFile.getParent();
System.out.println("Current executable directory: " + currentDir);
标签:java,String,System,maven,native,graalvm From: https://www.cnblogs.com/imliuyu/p/18346720