I have a little Java application that implements a RESTful API using Micronaut 2.0.0. Under the hood, it uses Redisson 3.13.1 to go to Redis. Redisson, in turn, uses Netty (4.1.49).
The application works fine in a 'classic' java (on HotSpot, both Java 8 and 11).
I'm trying to build a native image out of this application using GraalVM.
The command is approximately like this:
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
Here is what I get:
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)
That's just a part of the output, it also produces similar reports on other 3 errors.
I'm still struggling to understand the issue, but I suppose that, as java.net.InetAddress
has native methods in it, neither it nor its subclass java.net.Inet4Address
can be initialized at build time. This means that an instance of Inet4Address
cannot be visible for a code that is initialized at build time (at initialization stage, in Java terms). And the native image builder found a way that kinda reaches a point where such an object is visible. It even shows the trace, but the thing is that ClusterConnectionManager$1
is a Runnable
that is only submitted to an Executor
at runtime (waaaay after the static initialization).
How do you debug such situations? Namely:
PS. If I add --initialize-at-run-time=java.net.InetAddress
, it fails differently:
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 reports itself as build 25.252-b09-jvmci-20.1-b02, mixed mode
.
PPS. I found this No instances of ... are allowed in the image heap as this class should be initialized at image runtime and it seems that the Quarkus issue was fixed. But I still do not understand how to fix the issue at hand. Any help would be appreciated.
TLDR; there is a small section with a summary at the end of the answer.
In Java, every class must be initialized before usage. Initialization means executing static field initializers and static initialization blocks. In a standard JVM (like HotSpot), this happens at runtime, of course.
But with a native image, you have two alternatives. A class may still be initialized at runtime, or its initialization may be carried out at build time. The latter has an obvious benefit of avoiding this work at native image startup, which makes the image start faster. But for some classes it does not make sense to initialize them at build time. Such an example could be a class that makes some decisions in its initialization (create an instance of this or that class, for example) based on environment (env variables, config files, etc).
There are some restrictions in regard to choosing between build/run time initialization alternatives:
Since version 19.3.0, native-image
tool requires that java.net.InetAddress
class is always initialized at runtime.
This (by restriction 2) means that its subclasses, java.net.Inet4Address
and java.net.Inet6Address
must also be
initialized at runtime, which in turn (by restriction 4) means that you cannot have any InetAddress
referenced
by a build-time initialized class.
All the build failures we encounter here are caused by this same problem: either Inet4Address
or Inet6Address
in image heap.
It turns out that netty-codec-http
contains the following
Args = --initialize-at-build-time=io.netty \
in its native-image.properties
below META-INF
, and micronaut has netty-codec-http
as a dependency, so all
io.netty
classes are by default initialized at build time (as native-image
tool respects such
native-image.properties
files).
Here https://github.com/rpuch/netty-InetAddress-native-image-diagnosing is a project that models the problem and which
I further use to show how to solve the problem. Its main()
method follows:
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();
}
It causes the same effects (regarding Netty) that the following code does:
Config config = new Config();
config.useSingleServer().setAddress(redisUri);
config.useSingleServer().setPassword(redisPassword);
return Redisson.createReactive(config);
This project also has --initialize-at-build-time=io.netty
in its build script to emulate micronaut-based project
behavior.
So it is a useful substitute for the original project that brought this question to light.
I'm using version 20.2.0 here (the most recent released version as of the moment of writing).
Build fails with the following error:
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
is
return LOCALHOST_ADDRESS;
and it references the static field named LOCALHOST_ADDRESS
of type InetAddress
. Let's avoid its initialization
at build time by adding the following to the native-image
command`:
--initialize-at-run-time=io.netty.resolver.dns.DnsNameResolver
The error goes away.
Now there is another one:
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
is
private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT;
Let's postpone HostsFileEntriesResolver
initialization:
--initialize-at-run-time=io.netty.resolver.HostsFileEntriesResolver
Now, there is another error:
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
references NetUtil.LOCALHOST6
. NetUtil
is initialized at build time, but its fields
LOCALHOST4
and LOCALHOST6
contain instances of Inet4Address
and Inet6Address
, respectively. This can be solved
by a substitution. We just add the following class to our project:
@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");
}
}
}
}
The idea is to replace loads of the problematic fields with method invocations controlled by us. This is achieved
via a substitution (note @TargetClass
, @Alias
and @InjectAccessors
). As a result, the InetAddress
values
are not stored in image heap anymore. The error goes away.
We now have another one:
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)
As it can be seen from the code of InternetProtocolFamily
, each enum constant stores an instance of InetAddress
,
so if any class initialized at build time initializes InternetProtocolFamily
, the image heap gets polluted with
InetAddress
instance. This can also be solved with a substitution:
@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
}
}
}
The error goes away.
There is another one this time:
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)
First, let's move initialization of io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
to
run-time:
--initialize-at-run-time=io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider
Now, the error is similar, but still slightly different:
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)
Ok, let's move initialization of io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder
to
run-time as well:
'--initialize-at-run-time=io.netty.resolver.dns.DnsServerAddressStreamProviders$DefaultProviderHolder'
(note the single quotes: without them $
and characters following it will be interpreted by sh
and replaced with an
empty string).
The error goes away.
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).
Now we have this:
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
Ok, it's NetUtil.LOCALHOST
being referenced, so let's add a substitution for it as well (to 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;
}
}
This makes the final error go away.
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.
@RecomputeFieldValue
, but it's more
restricted, and I could not make it work in this Netty-related task.PS. Substitution-related code is inspired by https://github.com/quarkusio/quarkus/pull/5353/files
PPS. Item 5 solution is inspired by @NicolasFilotto
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With