构建本机映像时如何调试 'No instances of ... are allowed in the image heap'?
How do you debug a 'No instances of ... are allowed in the image heap' when building a native image?
我有一个 Java 小应用程序,它使用 Micronaut 2.0.0 实现了 RESTful API。在引擎盖下,它使用 Redisson 3.13.1 去 Redis。反过来,Redisson 使用 Netty (4.1.49)。
该应用程序在 'classic' 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.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
是一个 Runnable
,它仅在运行时(静态初始化后的 waaaay)提交给 Executor
。
你如何调试这种情况?即:
- 如何找到罪魁祸首?
- 找到罪魁祸首后如何解决?
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。我发现了这个 并且 Quarkus 问题似乎已解决。但我仍然不明白如何解决手头的问题。任何帮助将不胜感激。
TLDR;答案末尾有一小段总结。
一点理论
在Java中,每个class必须在使用前初始化。初始化意味着执行静态字段初始化器
和静态初始化块。在标准 JVM(如 HotSpot)中,这当然发生在 运行 时间。
但是对于原生图像,您有两种选择。 A class 可能仍会在 运行 时被初始化,或者它的初始化
可以在构建时执行。后者有一个明显的好处,可以避免在本机图像启动时进行这项工作,
这使得图像启动更快。但是对于某些 classes 在构建时初始化它们没有意义。
这样的例子可以是 class 在初始化时做出一些决定(创建这个或那个的实例
class,例如)基于环境(环境变量、配置文件等)。
在 build/run 时间初始化选项之间进行选择有一些限制:
- 如果 class 在构建时初始化,则它的所有超class 必须在构建时初始化
- 如果一个 class 在 运行 时间初始化,它的所有子 class 必须在 运行 时间
初始化
- 某些 classes 必须始终在 运行 时间初始化(见下文)
- 在运行时间初始化的class实例不能存在于图像堆中(这意味着不存在
build-time 已初始化 class 或其实例可以(直接或间接)引用这样的 runtime-initialized
class实例)
InetAddress 问题
自版本 19.3.0 起,native-image
工具要求 java.net.InetAddress
class 始终在 运行 时初始化。
这(根据限制 2)意味着它的子 classes、java.net.Inet4Address
和 java.net.Inet6Address
也必须是
在 运行 时间初始化,这反过来(通过限制 4)意味着您不能引用任何 InetAddress
由 build-time 初始化 class.
我们在这里遇到的所有构建失败都是由同一个问题引起的:Inet4Address
或 Inet6Address
在图像堆中。
但是为什么 Netty-related classes 试图在构建时初始化?
原来netty-codec-http
包含以下
Args = --initialize-at-build-time=io.netty \
在其 native-image.properties
下面 META-INF
,而 micronaut 有 netty-codec-http
作为依赖项,所以所有
io.netty
classes 默认在构建时初始化(因为 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-based 项目
行为。
所以它是揭示这个问题的原始项目的有用替代品。
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[=13=]0(Bootstrap.java:46)
at io.netty.bootstrap.Bootstrap.operationComplete(Bootstrap.java:180)
DnsNameResolver:659
是
return LOCALHOST_ADDRESS;
并且它引用了类型为 InetAddress
的名为 LOCALHOST_ADDRESS
的静态字段。让我们避免它的初始化
在构建时通过将以下内容添加到 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.LOCALHOST6
。 NetUtil
在构建时初始化,但它的字段
LOCALHOST4
和 LOCALHOST6
分别包含 Inet4Address
和 Inet6Address
的实例。这个可以解决
通过替换。我们只需将以下 class 添加到我们的项目中:
@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
的代码可以看出,每个enum常量存储了InetAddress
的一个实例,
因此,如果在构建时初始化的任何 class 初始化 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.nameServerAddressStream(DnsServerAddressStreamProviders.java:131)
at io.netty.resolver.dns.DnsNameResolver.doResolveAllUncached0(DnsNameResolver.java:1070)
首先,让我们将io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
的初始化移动到
run-time:
--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.currentProvider of
constant io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder@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
的初始化移到
run-time 还有:
'--initialize-at-run-time=io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder'
(注意单引号:没有它们 $
和它后面的字符将被 sh
和 r放置一个
空字符串)。
错误消失。
Please note that the order turned out to be important here. When I first moved
io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder
initialization to run-time but did not
touch io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
initialization, the error report did not change
a bit. So it takes a little patience and experimenting (or some knowledge which I do not have, alas).
现在我们有这个:
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;
}
}
这使得最后的错误消失了。
Thanks to @NicolasFilotto for suggestions on item 5, I like his solution a lot more than the original, and actually
item 5 is an implementation of his ideas.
技巧总结
- 首先,你可以找到一个class,它被移动到运行时间初始化阶段,导致失败消失。
为此,您可以在提供的堆栈跟踪中跟踪引用。最好考虑的候选者是静态字段。
- 如果第 1 项没有帮助,您可以尝试创建替代品
- 理论上也有可能使用更轻的变体:
@RecomputeFieldValue
,但它更
受限,我无法让它在这个 Netty-related 任务中工作。
PS。 Substitution-related 代码的灵感来自 https://github.com/quarkusio/quarkus/pull/5353/files
PPS。第 5 项解决方案的灵感来自@NicolasFilotto
我有一个 Java 小应用程序,它使用 Micronaut 2.0.0 实现了 RESTful API。在引擎盖下,它使用 Redisson 3.13.1 去 Redis。反过来,Redisson 使用 Netty (4.1.49)。
该应用程序在 'classic' 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.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
是一个 Runnable
,它仅在运行时(静态初始化后的 waaaay)提交给 Executor
。
你如何调试这种情况?即:
- 如何找到罪魁祸首?
- 找到罪魁祸首后如何解决?
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。我发现了这个
TLDR;答案末尾有一小段总结。
一点理论
在Java中,每个class必须在使用前初始化。初始化意味着执行静态字段初始化器 和静态初始化块。在标准 JVM(如 HotSpot)中,这当然发生在 运行 时间。
但是对于原生图像,您有两种选择。 A class 可能仍会在 运行 时被初始化,或者它的初始化 可以在构建时执行。后者有一个明显的好处,可以避免在本机图像启动时进行这项工作, 这使得图像启动更快。但是对于某些 classes 在构建时初始化它们没有意义。 这样的例子可以是 class 在初始化时做出一些决定(创建这个或那个的实例 class,例如)基于环境(环境变量、配置文件等)。
在 build/run 时间初始化选项之间进行选择有一些限制:
- 如果 class 在构建时初始化,则它的所有超class 必须在构建时初始化
- 如果一个 class 在 运行 时间初始化,它的所有子 class 必须在 运行 时间 初始化
- 某些 classes 必须始终在 运行 时间初始化(见下文)
- 在运行时间初始化的class实例不能存在于图像堆中(这意味着不存在 build-time 已初始化 class 或其实例可以(直接或间接)引用这样的 runtime-initialized class实例)
InetAddress 问题
自版本 19.3.0 起,native-image
工具要求 java.net.InetAddress
class 始终在 运行 时初始化。
这(根据限制 2)意味着它的子 classes、java.net.Inet4Address
和 java.net.Inet6Address
也必须是
在 运行 时间初始化,这反过来(通过限制 4)意味着您不能引用任何 InetAddress
由 build-time 初始化 class.
我们在这里遇到的所有构建失败都是由同一个问题引起的:Inet4Address
或 Inet6Address
在图像堆中。
但是为什么 Netty-related classes 试图在构建时初始化?
原来netty-codec-http
包含以下
Args = --initialize-at-build-time=io.netty \
在其 native-image.properties
下面 META-INF
,而 micronaut 有 netty-codec-http
作为依赖项,所以所有
io.netty
classes 默认在构建时初始化(因为 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-based 项目
行为。
所以它是揭示这个问题的原始项目的有用替代品。
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[=13=]0(Bootstrap.java:46)
at io.netty.bootstrap.Bootstrap.operationComplete(Bootstrap.java:180)
DnsNameResolver:659
是
return LOCALHOST_ADDRESS;
并且它引用了类型为 InetAddress
的名为 LOCALHOST_ADDRESS
的静态字段。让我们避免它的初始化
在构建时通过将以下内容添加到 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.LOCALHOST6
。 NetUtil
在构建时初始化,但它的字段
LOCALHOST4
和 LOCALHOST6
分别包含 Inet4Address
和 Inet6Address
的实例。这个可以解决
通过替换。我们只需将以下 class 添加到我们的项目中:
@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
的代码可以看出,每个enum常量存储了InetAddress
的一个实例,
因此,如果在构建时初始化的任何 class 初始化 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.nameServerAddressStream(DnsServerAddressStreamProviders.java:131)
at io.netty.resolver.dns.DnsNameResolver.doResolveAllUncached0(DnsNameResolver.java:1070)
首先,让我们将io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
的初始化移动到
run-time:
--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.currentProvider of
constant io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder@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
的初始化移到
run-time 还有:
'--initialize-at-run-time=io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder'
(注意单引号:没有它们 $
和它后面的字符将被 sh
和 r放置一个
空字符串)。
错误消失。
Please note that the order turned out to be important here. When I first moved
io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder
initialization to run-time but did not touchio.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
initialization, the error report did not change a bit. So it takes a little patience and experimenting (or some knowledge which I do not have, alas).
现在我们有这个:
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;
}
}
这使得最后的错误消失了。
Thanks to @NicolasFilotto for suggestions on item 5, I like his solution a lot more than the original, and actually item 5 is an implementation of his ideas.
技巧总结
- 首先,你可以找到一个class,它被移动到运行时间初始化阶段,导致失败消失。 为此,您可以在提供的堆栈跟踪中跟踪引用。最好考虑的候选者是静态字段。
- 如果第 1 项没有帮助,您可以尝试创建替代品
- 理论上也有可能使用更轻的变体:
@RecomputeFieldValue
,但它更 受限,我无法让它在这个 Netty-related 任务中工作。
PS。 Substitution-related 代码的灵感来自 https://github.com/quarkusio/quarkus/pull/5353/files
PPS。第 5 项解决方案的灵感来自@NicolasFilotto