构建本机映像时,如何调试“映像堆中不允许...的实例”?

时间:2020-08-09 15:43:09

标签: java netty graalvm-native-image

我有一个小的Java应用程序,它使用Micronaut 2.0.0实现RESTful API。在后台,它使用Redisson 3.13.1转到Redis。反过来,Redisson使用Netty(4.1.49)。

该应用程序在“经典” java(在HotSpot上为Java 8和11)上都可以正常工作。

我正在尝试使用GraalVM在此应用程序中构建本机映像。

该命令大致为 ,如下所示:

native-image --no-server --no-fallback -H:+TraceClassInitialization -H:+PrintClassInitialization --report-unsupported-elements-at-runtime --initialize-at-build-time=reactor.core.publisher.Flux,reactor.core.publisher.Mono -H:ConfigurationFileDirectories=target/config -cp target/app-1.0.0-SNAPSHOT.jar com.app.AppApplication target/app

这就是我得到的:

Error: Unsupported features in 4 methods
Detailed message:
Error: No instances of java.net.Inet4Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace: Object was reached by 
    reading field io.netty.channel.socket.InternetProtocolFamily.localHost of
        constant io.netty.channel.socket.InternetProtocolFamily@593f1f62 reached by 
    scanning method io.netty.resolver.dns.DnsNameResolver.preferredAddressType(DnsNameResolver.java:481)
Call path from entry point to io.netty.resolver.dns.DnsNameResolver.preferredAddressType(ResolvedAddressTypes): 
    at io.netty.resolver.dns.DnsNameResolver.preferredAddressType(DnsNameResolver.java:478)
    at io.netty.resolver.dns.DnsNameResolver.<init>(DnsNameResolver.java:436)
    at io.netty.resolver.dns.DnsNameResolverBuilder.build(DnsNameResolverBuilder.java:473)
    at io.netty.resolver.dns.DnsAddressResolverGroup.newNameResolver(DnsAddressResolverGroup.java:111)
    at io.netty.resolver.dns.DnsAddressResolverGroup.newResolver(DnsAddressResolverGroup.java:91)
    at io.netty.resolver.dns.DnsAddressResolverGroup.newResolver(DnsAddressResolverGroup.java:76)
    at io.netty.resolver.AddressResolverGroup.getResolver(AddressResolverGroup.java:70)
    at org.redisson.cluster.ClusterConnectionManager$1.run(ClusterConnectionManager.java:251)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:125)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:75)
    at com.oracle.svm.core.JavaMainWrapper.runCore(JavaMainWrapper.java:141)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:184)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

这只是输出的一部分,它还会针对其他3个错误生成类似的报告。

我仍在努力理解这个问题,但是我想,由于java.net.InetAddress中包含本机方法,因此它或其子类java.net.Inet4Address都无法在构建时初始化。这意味着Inet4Address的实例对于在构建时初始化的代码(在Java的初始化阶段)不可见。并且本机图像生成器找到了一种方法,可以使这种对象可见。它甚至显示了跟踪,但事实是ClusterConnectionManager$1是一个Runnable,仅在运行时(静态初始化之后就被提交给Executor)。

您如何调试这种情况?即:

  1. 您如何找到罪魁祸首?
  2. 找到罪魁祸首时如何解决?

PS。如果我添加--initialize-at-run-time=java.net.InetAddress,它会以不同的方式失败:

Error: The class java.net.InetAddress has already been initialized; it is too late 
to register java.net.InetAddress for build-time initialization (from the command 
line). java.net.InetAddress has been initialized without the native-image 
initialization instrumentation and the stack trace can't be tracked. Try avoiding 
this conflict by avoiding to initialize the class that caused initialization of 
java.net.InetAddress or by not marking java.net.InetAddress for build-time 
initialization.

Java将自身报告为build 25.252-b09-jvmci-20.1-b02, mixed mode

PPS。我找到了这个No instances of ... are allowed in the image heap as this class should be initialized at image runtime,看来Quarkus问题已解决。但是我仍然不知道如何解决当前问题。任何帮助将不胜感激。

1 个答案:

答案 0 :(得分:8)

TLDR; ;答案末尾有一小部分带有摘要。

一点理论

在Java中,必须在使用每个类之前对其进行初始化。初始化意味着执行静态字段初始化程序 和静态初始化块。当然,在标准的JVM(例如HotSpot)中,这是在运行时发生的。

但是对于本机映像,您有两种选择。一个类可能仍在运行时初始化,或其初始化 可以在构建时进行。后者具有避免在本机映像启动时避免这项工作的明显好处, 这样可以使图像启动更快。但是对于某些类,在构建时初始化它们是没有意义的。 这样的示例可能是一个在初始化时做出一些决定的类(创建this或 例如,基于环境(环境变量,配置文件等)的类。

在构建/运行时初始化替代方案之间进行选择存在一些限制:

  • 如果一个类在构建时初始化,那么它的所有超类必须在构建时初始化
  • 如果在运行时初始化一个类,则必须在运行时初始化其所有子类
  • 某些类必须始终在运行时初始化(请参见下文)
  • 在运行时初始化的类的实例不能存在于图像堆中(这意味着否 构建时初始化的类或其实例可以(直接或间接)引用这样的运行时初始化 类实例

InetAddress问题

从19.3.0版开始,native-image工具要求java.net.InetAddress类始终在运行时进行初始化。 (通过限制2)意味着它的子类java.net.Inet4Addressjava.net.Inet6Address也必须是 在运行时初始化,这又(通过限制4)意味着您不能引用任何InetAddress 通过构建时初始化的类。

我们在此处遇到的所有构建失败都是由同一问题引起的:Inet4AddressInet6Address 在图像堆中。

但是为什么要在构建时初始化与Netty相关的类?

事实证明netty-codec-http包含以下内容

Args = --initialize-at-build-time=io.netty \

native-image.properties下面的META-INF中,并且micronaut将netty-codec-http作为依赖项,因此所有 io.netty类默认在构建时初始化(因为native-image工具尊重 native-image.properties个文件)。

模型项目

这里https://github.com/rpuch/netty-InetAddress-native-image-diagnosing是一个为问题建模的项目, 我将进一步说明如何解决该问题。其main()方法如下:

public static void main(String[] args) throws Exception {
    NioEventLoopGroup group = new NioEventLoopGroup(1, new DefaultThreadFactory("netty"));

    DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup(NioDatagramChannel.class,
            DnsServerAddressStreamProviders.platformDefault());
    AddressResolver<InetSocketAddress> resolver = resolverGroup.getResolver(group.next());
    System.out.println(resolver);

    resolver.close();
    group.shutdownGracefully().get();
}

它会产生与以下代码相同的效果(关于Netty):

    Config config = new Config();
    config.useSingleServer().setAddress(redisUri);
    config.useSingleServer().setPassword(redisPassword);

    return Redisson.createReactive(config);

此项目的构建脚本中还包含--initialize-at-build-time=io.netty,以模拟基于Micronaut的项目 行为。

因此,它是代替原来的项目的有用的替代方法。

GraalVM版本

我在这里使用的是20.2.0版(截至撰写本文时为最新发行版)。

诊断和修复

1

构建失败,并出现以下错误:

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: No instances of java.net.Inet4Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace:
    at parsing io.netty.resolver.dns.DnsNameResolver.resolveHostsFileEntry(DnsNameResolver.java:659)
Call path from entry point to io.netty.resolver.dns.DnsNameResolver.resolveHostsFileEntry(String):
    at io.netty.resolver.dns.DnsNameResolver.resolveHostsFileEntry(DnsNameResolver.java:651)
    at io.netty.resolver.dns.DnsNameResolver.doResolve(DnsNameResolver.java:884)
    at io.netty.resolver.dns.DnsNameResolver.doResolve(DnsNameResolver.java:733)
    at io.netty.resolver.SimpleNameResolver.resolve(SimpleNameResolver.java:61)
    at io.netty.resolver.SimpleNameResolver.resolve(SimpleNameResolver.java:53)
    at io.netty.resolver.InetSocketAddressResolver.doResolve(InetSocketAddressResolver.java:55)
    at io.netty.resolver.InetSocketAddressResolver.doResolve(InetSocketAddressResolver.java:31)
    at io.netty.resolver.AbstractAddressResolver.resolve(AbstractAddressResolver.java:106)
    at io.netty.bootstrap.Bootstrap.doResolveAndConnect0(Bootstrap.java:206)
    at io.netty.bootstrap.Bootstrap.access$000(Bootstrap.java:46)
    at io.netty.bootstrap.Bootstrap$1.operationComplete(Bootstrap.java:180)

DnsNameResolver:659

return LOCALHOST_ADDRESS;

,它引用类型为LOCALHOST_ADDRESS的名为InetAddress的静态字段。让我们避免其初始化 在构建时,将以下内容添加到native-image命令`中:

--initialize-at-run-time=io.netty.resolver.dns.DnsNameResolver

错误消失了。

2

现在有另一个:

Error: No instances of java.net.Inet6Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace: Object was reached by
    reading field java.util.HashMap$Node.value of
        constant java.util.HashMap$Node@26eb0f30 reached by
    indexing into array
        constant java.util.HashMap$Node[]@63e95621 reached by
    reading field java.util.HashMap.table of
        constant java.util.HashMap@563992d1 reached by
    reading field java.util.Collections$UnmodifiableMap.m of
        constant java.util.Collections$UnmodifiableMap@38a9945c reached by
    reading field io.netty.resolver.DefaultHostsFileEntriesResolver.inet6Entries of
        constant io.netty.resolver.DefaultHostsFileEntriesResolver@7ef4ba7e reached by
    scanning method io.netty.resolver.dns.DnsNameResolverBuilder.<init>(DnsNameResolverBuilder.java:56)
Call path from entry point to io.netty.resolver.dns.DnsNameResolverBuilder.<init>():
    at io.netty.resolver.dns.DnsNameResolverBuilder.<init>(DnsNameResolverBuilder.java:68)
    at io.netty.resolver.dns.DnsAddressResolverGroup.<init>(DnsAddressResolverGroup.java:54)
    at Main.main(Main.java:18)

DnsNameResolverBuilder:56

private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT;

让我们推迟HostsFileEntriesResolver初始化:

--initialize-at-run-time=io.netty.resolver.HostsFileEntriesResolver

3

现在,还有另一个错误:

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: No instances of java.net.Inet6Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace:
    at parsing io.netty.resolver.dns.DnsQueryContextManager.getOrCreateContextMap(DnsQueryContextManager.java:111)
Call path from entry point to io.netty.resolver.dns.DnsQueryContextManager.getOrCreateContextMap(InetSocketAddress):
    at io.netty.resolver.dns.DnsQueryContextManager.getOrCreateContextMap(DnsQueryContextManager.java:96)

DnsQueryContextManager:111参考NetUtil.LOCALHOST6NetUtil在构建时初始化,但是其字段 LOCALHOST4LOCALHOST6分别包含Inet4AddressInet6Address的实例。这可以解决 由替代。我们只需将以下类添加到我们的项目中:

@TargetClass(NetUtil.class)
final class NetUtilSubstitutions {
    @Alias
    @InjectAccessors(NetUtilLocalhost4Accessor.class)
    public static Inet4Address LOCALHOST4;

    @Alias
    @InjectAccessors(NetUtilLocalhost6Accessor.class)
    public static Inet6Address LOCALHOST6;

    private static class NetUtilLocalhost4Accessor {
        static Inet4Address get() {
            // using https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
            return NetUtilLocalhost4LazyHolder.LOCALHOST4;
        }

        static void set(Inet4Address ignored) {
            // a no-op setter to avoid exceptions when NetUtil is initialized at run-time
        }
    }

    private static class NetUtilLocalhost4LazyHolder {
        private static final Inet4Address LOCALHOST4;

        static {
            byte[] LOCALHOST4_BYTES = {127, 0, 0, 1};
            // Create IPv4 loopback address.
            try {
                LOCALHOST4 = (Inet4Address) InetAddress.getByAddress("localhost", LOCALHOST4_BYTES);
            } catch (Exception e) {
                // We should not get here as long as the length of the address is correct.
                PlatformDependent.throwException(e);
                throw new IllegalStateException("Should not reach here");
            }
        }
    }

    private static class NetUtilLocalhost6Accessor {
        static Inet6Address get() {
            // using https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
            return NetUtilLocalhost6LazyHolder.LOCALHOST6;
        }

        static void set(Inet6Address ignored) {
            // a no-op setter to avoid exceptions when NetUtil is initialized at run-time
        }
    }
    
    private static class NetUtilLocalhost6LazyHolder {
        private static final Inet6Address LOCALHOST6;

        static {
            byte[] LOCALHOST6_BYTES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
            // Create IPv6 loopback address.
            try {
                LOCALHOST6 = (Inet6Address) InetAddress.getByAddress("localhost", LOCALHOST6_BYTES);
            } catch (Exception e) {
                // We should not get here as long as the length of the address is correct.
                PlatformDependent.throwException(e);
                throw new IllegalStateException("Should not reach here");
            }
        }
    }
}

这个想法是用我们控制的方法调用来替换问题字段的负载。这实现了 通过替换(注意@TargetClass@Alias@InjectAccessors)。结果,InetAddress值 不再存储在图像堆中。错误消失了。

4

我们现在有了另一个:

Error: No instances of java.net.Inet6Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace: Object was reached by
    reading field io.netty.channel.socket.InternetProtocolFamily.localHost of
        constant io.netty.channel.socket.InternetProtocolFamily@5dc39065 reached by
    scanning method io.netty.resolver.dns.DnsNameResolver.preferredAddressType(DnsNameResolver.java:487)
Call path from entry point to io.netty.resolver.dns.DnsNameResolver.preferredAddressType(ResolvedAddressTypes):
    at io.netty.resolver.dns.DnsNameResolver.preferredAddressType(DnsNameResolver.java:481)

InternetProtocolFamily的代码可以看出,每个枚举常量都存储一个InetAddress的实例, 因此,如果在构建时初始化的任何类都初始化InternetProtocolFamily,则图像堆将被污染 InetAddress个实例。这也可以通过替换来解决:

@TargetClass(InternetProtocolFamily.class)
final class InternetProtocolFamilySubstitutions {
    @Alias
    @InjectAccessors(InternetProtocolFamilyLocalhostAccessor.class)
    private InetAddress localHost;

    private static class InternetProtocolFamilyLocalhostAccessor {
        static InetAddress get(InternetProtocolFamily family) {
            switch (family) {
                case IPv4:
                    return NetUtil.LOCALHOST4;
                case IPv6:
                    return NetUtil.LOCALHOST6;
                default:
                    throw new IllegalStateException("Unsupported internet protocol family: " + family);
            }
        }

        static void set(InternetProtocolFamily family, InetAddress address) {
            // storing nothing as the getter derives all it needs from its argument
        }
    }
}

错误消失了。

5

这一次还有一个:

Error: No instances of java.net.Inet4Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Detailed message:
Trace: Object was reached by
    reading field java.net.InetSocketAddress$InetSocketAddressHolder.addr of
        constant java.net.InetSocketAddress$InetSocketAddressHolder@34913c36 reached by
    reading field java.net.InetSocketAddress.holder of
        constant java.net.InetSocketAddress@ad1fe10 reached by
    reading field io.netty.resolver.dns.SingletonDnsServerAddresses.address of
        constant io.netty.resolver.dns.SingletonDnsServerAddresses@79fd599 reached by
    scanning method io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.nameServerAddressStream(DefaultDnsServerAddressStreamProvider.java:115)
Call path from entry point to io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.nameServerAddressStream(String):
    at io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.nameServerAddressStream(DefaultDnsServerAddressStreamProvider.java:115)
    at io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder$1.nameServerAddressStream(DnsServerAddressStreamProviders.java:131)
    at io.netty.resolver.dns.DnsNameResolver.doResolveAllUncached0(DnsNameResolver.java:1070)

首先,让我们将io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider的初始化移动到 运行时:

--initialize-at-run-time=io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider

现在,错误是相似的,但仍然略有不同:

Error: No instances of java.net.Inet4Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Trace: Object was reached by
    reading field java.net.InetSocketAddress$InetSocketAddressHolder.addr of
        constant java.net.InetSocketAddress$InetSocketAddressHolder@5537c5de reached by
    reading field java.net.InetSocketAddress.holder of
        constant java.net.InetSocketAddress@fb954f8 reached by
    reading field io.netty.resolver.dns.SingletonDnsServerAddresses.address of
        constant io.netty.resolver.dns.SingletonDnsServerAddresses@3ec9baab reached by
    reading field io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.defaultNameServerAddresses of
        constant io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider@1b7f0339 reached by
    reading field io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder$1.currentProvider of
        constant io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder$1@2d249be7 reached by
    scanning method io.netty.resolver.dns.DnsServerAddressStreamProviders.unixDefault(DnsServerAddressStreamProviders.java:104)
Call path from entry point to io.netty.resolver.dns.DnsServerAddressStreamProviders.unixDefault():
    at io.netty.resolver.dns.DnsServerAddressStreamProviders.unixDefault(DnsServerAddressStreamProviders.java:104)
    at io.netty.resolver.dns.DnsServerAddressStreamProviders.platformDefault(DnsServerAddressStreamProviders.java:100)
    at Main.main(Main.java:18)

好吧,让我们将io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder的初始化移动到 以及运行时:

'--initialize-at-run-time=io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder'

(请注意单引号:没有$及其后的字符将由sh解释并替换为 空字符串)。

错误消失了。

请注意,在这里顺序很重要。我第一次搬家时 io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder初始化为运行时,但未初始化 触摸io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider初始化,错误报告未更改 一点点。因此,这需要一点耐心和实验性((,或者我没有的一些知识)。

现在我们有了这个

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: No instances of java.net.Inet6Address are allowed in the image heap as this class should be initialized at image runtime. Object has been initialized without the native-image initialization instrumentation and the stack trace can't be tracked.
Detailed message:
Trace:
    at parsing io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.<clinit>(DefaultDnsServerAddressStreamProvider.java:87)
Call path from entry point to io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.<clinit>():
    no path found from entry point to target method

好吧,它是NetUtil.LOCALHOST的引用,因此我们也要为其添加一个替代项(到NetUtilSubstitutions):

@Alias
@InjectAccessors(NetUtilLocalhostAccessor.class)
public static InetAddress LOCALHOST;

// NOTE: this is the simpliest implementation I could invent to just demonstrate the idea; it is probably not
// too efficient. An efficient implementation would only have getter and it would compute the InetAddress
// there; but the post is already very long, and NetUtil.LOCALHOST computation logic in Netty is rather cumbersome.
private static class NetUtilLocalhostAccessor {
    private static volatile InetAddress ADDR;

    static InetAddress get() {
        return ADDR;
    }

    static void set(InetAddress addr) {
        ADDR = addr;
    }
}

这使最后的错误消失了。

感谢@NicolasFilotto对第5项的建议,我非常喜欢他的解决方案,而实际上 项目5是他的想法的实现。

技术摘要

  1. 首先,您可以找到一个类,该类已移至运行时初始化阶段,导致无法解决。 为此,您可以在提供的堆栈跟踪中跟踪引用。静态字段是最好的考虑因素。
  2. 如果第1项没有帮助,您可以尝试创建替代项
  3. 理论上也可以使用更轻的变体:@RecomputeFieldValue,但更多 受限制,我无法使其完成与Netty相关的任务。

PS。与替换相关的代码受https://github.com/quarkusio/quarkus/pull/5353/files

的启发

PPS。第5项解决方案的灵感来自@NicolasFilotto