Java AOT之Graalvm native image介绍以及简单长连接服务实践

Java AOT之Graalvm native image介绍以及简单长连接服务实践

·

2 min read

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 和其他支持的语言可以直接互操作,并在同一内存空间中来回传递数据。 image.png

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社区版

  1. 通过Github下载GraalVM
  2. 解压到/Library/Java/JavaVirtualMachines目录下
  3. 打开JavaVirtualMachines目录下Contents/Home/bin校验java -version,添加JAVA_HOME环境变量
  4. 安装native-image gu install native-image,编译native image依赖于本地工具链,确保本地有安装glibc-develzlib-develgcc

长链服务

服务通过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即可完成字节码编译及静态编译,编译结果如下,可以看到整个静态编译耗时还是比较长的,主要分布在静态分析和编译这两个阶段。 image.png 生成的native文件位于target包中,大小约17M,进入target目录,可用./mini-connector直接执行。

结论

通过Native image,可以将java服务启动时间压缩数十倍,且生成的二进制文件大小也优于包括所有依赖的jar包。

与传统Java运行模型相比,静态编译运行通过AOT避免了JIT的CPU开销,也避免了传统运行模型中一定存在的解释执行问题,使得程序性能较稳定。通过轻量化SubstrateVM实现,且也静态编译至native image中,提供了较快的vm性能和启动速度。

但是,任何技术都有优缺点。而Graalvm静态编译则需要面临解决动态类加载、反射、动态代理等动态特性的适配的问题。另外通过native运行的程序,将不再适用面向传统JVM程序的调试、监控、Agent等功能。