Java语言有20多年的发展历史,拥有众多优秀的特性如面向对象、安全、解释性、平台无关等,该语言以及其强大的生态使其成为最重要的网络编程语言。但是随着近年来技术架构的发展,微服务逐渐趋向云原生及Serverless化,使得Java也面临如下挑战:启动缓慢、内存占用大、预热问题。
Java服务启动时首先要启动一个JVM虚拟机,然后虚拟机会加载字节码,中间还包括类的加载解析初始化。JVM运行字节码运行时有解释执行和编译两种执行方式:当系统刚启动时,JVM会以解释执行并检测热点代码,热点代码会通过c1c2编译器进行编译成本地二进制代码。通过这样的机制,Java实现了很多诸如反射、动态代理等运行时的机制,例如可以在程序运行过程中加载并编译一段代码,而这个在静态编译中却不可想象。运行时编译还有个好处是可以做比较激进的编译优化,通过c2编译器和分支预测Java在某些场景下可以表现出比c++更强劲的性能。但这些优点也是有价的,包括启动耗时、预热耗时以及服务内存占用。Graalvm
Graalvm是Oracle公司提供的一个高性能、云原生、多语言的虚拟机。除了运行 Java 和基于 JVM 的语言之外,GraalVM 的语言实现框架 (Truffle)使得在 JVM 上运行 JavaScript、Ruby、Python 和许多其他流行语言成为可能。借助 GraalVM Truffle,Java 和其他支持的语言可以直接互操作,并在同一内存空间中来回传递数据。
Native Image是一种将Java代码提前编译为独立可执行文件的技术,此刻执行文件包括应用程序类、依赖、运行时库以及JDK静态连接的本机代码。Graalvm通过子模块SubstrateVM来支持Native Image,相比JVM其生成的程序具有更快的启动时间和更低的运行时开销。
SubstrateVM
SubstrateVM是GraalVM实现静态编译的基础,可以从支持静态编译以及运行时两方面来简单了解。
静态编译
应用程序、第三方库和JDK字节码共同组成了静态编译的输入,SubstrateVM会对输入进行静态分析,找到其中可达代码,然后可达代码将会有静态编译器进行编译,最终得到native image。值得注意的是由于只会编译可达的代码,所以其生成的文件相对会较小。静态分析输出是控制流图(Control Flow Graph)和类型流图(Type Flow Graph),其耗时也为整个编译流程中最长。
运行时
Native image运行需要提供垃圾回收、类初始化检查、一次处理、多线程等支持。SubstrateVM通过Java做轻量化VM运行时实现。并且通过静态编译,将运行时支持一起编译至native image中,需要注意的是GraalVM社区版只提供SerialGC垃圾收集器。
实践
通过安装使用GraalVM社区版,并创建一个echo的长连接服务来实践GraalVM native image,这里的环境是MacOS。
安装GraalVM社区版
- 通过Github下载GraalVM
- 解压到
/Library/Java/JavaVirtualMachines
目录下 - 打开
JavaVirtualMachines
目录下Contents/Home/bin
校验java -version
,添加JAVA_HOME环境变量 - 安装native-image
gu install native-image
,编译native image依赖于本地工具链,确保本地有安装glibc-devel
、zlib-devel
、gcc
长链服务
服务通过Netty实现一个echo服务器,主要包括以下类
启动类
public class Bootstrap {
public static void main(String[] args) {
ServerBootstrap server = new ServerBootstrap();
EventLoopGroup boss = new NioEventLoopGroup(1, newThreadFactory("netty-boss"));
EventLoopGroup worker = new NioEventLoopGroup(10, newThreadFactory("netty-worker"));
server.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline()
.addLast(new ProtocolDecoder())
.addLast(new ProtocolEncoder())
.addLast(new ServerHandler());
}
});
server.option(ChannelOption.SO_BACKLOG,1024);
server.option(ChannelOption.SO_REUSEADDR, true);
server.option(ChannelOption.SO_RCVBUF, 256 * 1024);
server.option(ChannelOption.SO_SNDBUF, 256 * 1024);
server.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
server.childOption(ChannelOption.SO_KEEPALIVE, true); server.childOption(ChannelOption.TCP_NODELAY,true);
server.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); server.childOption(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT);
try {
server.bind(16688).sync();
} catch (Exception e) {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
private static ThreadFactory newThreadFactory(final String name) {
return new ThreadFactory(){
private AtomicLong counter = new AtomicLong();
@Override
public Thread newThread(Runnable r) {
return new Thread(r, name + counter.incrementAndGet());
}
};
}
}
Handler
public class ServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("msg received: " + msg);
ctx.writeAndFlush("resp: " + msg);
}
}
Decoder
public class ProtocolDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() > 0) {
byte[] bytesReady = new byte[in.readableBytes()];
in.readBytes(bytesReady);
out.add(new String(bytesReady, "utf8"));
}
}
}
Encoder
public class ProtocolEncoder extends MessageToByteEncoder<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
out.writeBytes(msg.getBytes("utf8"));
}
}
pom.xml
xml
<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>com.github.binarybeing</groupId>
<artifactId>mini-connector</artifactId>
<version>0.1.0</version>
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<configuration>
<mainClass>Bootstrap</mainClass>
<imageName>mini-connector</imageName>
<buildArgs>
--no-fallback
--initialize-at-build-time=org.slf4j.MDC
--initialize-at-build-time=org.slf4j.LoggerFactory
--initialize-at-build-time=org.slf4j.impl.StaticLoggerBinder
--initialize-at-build-time=org.apache.log4j.helpers.Loader
--initialize-at-build-time=org.apache.log4j.Logger
--initialize-at-build-time=org.apache.log4j.helpers.LogLog
--initialize-at-build-time=org.apache.log4j.LogManager
--initialize-at-build-time=org.apache.log4j.spi.LoggingEvent
--initialize-at-build-time=org.slf4j.impl.Log4jLoggerFactory
--initialize-at-build-time=org.slf4j.impl.Log4jLoggerAdapter
--initialize-at-run-time=io.netty.channel.epoll.Epoll
--initialize-at-run-time=io.netty.channel.epoll.Native
--initialize-at-run-time=io.netty.channel.epoll.EpollEventLoop
--initialize-at-run-time=io.netty.channel.epoll.EpollEventArray
--initialize-at-run-time=io.netty.channel.DefaultFileRegion
--initialize-at-run-time=io.netty.channel.kqueue.KQueueEventArray
--initialize-at-run-time=io.netty.channel.kqueue.KQueueEventLoop
--initialize-at-run-time=io.netty.channel.kqueue.Native
--initialize-at-run-time=io.netty.channel.unix.Errors
--initialize-at-run-time=io.netty.channel.unix.IovArray
--initialize-at-run-time=io.netty.channel.unix.Limits
--initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger
--initialize-at-run-time=io.netty.channel.unix.Socket
--initialize-at-run-time=io.netty.channel.ChannelHandlerMask
--report-unsupported-elements-at-runtime
--allow-incomplete-classpath
--enable-url-protocols=http
-H:+ReportExceptionStackTraces
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.immomo.moa</groupId>
<artifactId>moa-dependency</artifactId>
<version>2.3.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<encoding>UTF-8</encoding>
<compilerVersion>1.8</compilerVersion>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>Bootstrap</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
</project>
先启动服务,使用nc工具测试功能,结果如下。
➜ ~ nc localhost 16688
hello
resp: hello
然后关闭服务器,用GraalVM执行静态编译,这里使用的是maven插件,执行mvn -Pnative -DskipTests package
即可完成字节码编译及静态编译,编译结果如下,可以看到整个静态编译耗时还是比较长的,主要分布在静态分析和编译这两个阶段。
生成的native文件位于target包中,大小约17M,进入target目录,可用./mini-connector
直接执行。
结论
通过Native image,可以将java服务启动时间压缩数十倍,且生成的二进制文件大小也优于包括所有依赖的jar包。
与传统Java运行模型相比,静态编译运行通过AOT避免了JIT的CPU开销,也避免了传统运行模型中一定存在的解释执行问题,使得程序性能较稳定。通过轻量化SubstrateVM实现,且也静态编译至native image中,提供了较快的vm性能和启动速度。 但是,任何技术都有优缺点。而Graalvm静态编译则需要面临解决动态类加载、反射、动态代理等动态特性的适配的问题。另外通过native运行的程序,将不再适用面向传统JVM程序的调试、监控、Agent等功能。