Skip to content

[Java] Async JIT compilation fails on JDK 25+ for composite types: Unsafe.objectFieldOffset called on hidden-nestmate codec class #3782

Description

@typat

Search before asking

  • I had searched in the issues and found no similar issues.

Version

fory-core 1.2.0; JDK 25 / 26 (reproduced on Zulu 26.28+63); withCompatible(true), requireClassRegistration(false), withAsyncCompilation(true), withCodegen(true) (default); Fory and the domain types share one classpath/classloader.

Component(s)

Java

Minimal reproduce step

a record Meta(Link link, Tag tag, Set<Flag> flags, Set<Mode> modes, String label) of nested records, registered on a buildThreadSafeForyPool with the config above, serialize(...) then Thread.sleep to let the compiler threads surface the exception. (I have a ~50-line standalone ForyRepro.java I can attach.)

What did you expect to see?

Serializing a registered composite type (a record with nested registered records) JIT-compiles its codec in the background, fully installs it (nested serializers injected into the codec's serializer/serializer1 fields) and produces no exceptions. The fory-jit-compiler-* threads stay alive and the generated codec is used on subsequent calls. I.e. identical behavior to JDK ≤ 24.

What did you see instead?

serialize(...) returns correct bytes (interpreter fallback), but the background compiler threads throw:

Exception in thread "fory-jit-compiler-2" java.lang.UnsupportedOperationException:
   can't get field offset on a hidden class:
   org.apache.fory.serializer.Serializer <pkg>.<Type>ForyCodec_0/0x….serializer
     at sun.misc.Unsafe.objectFieldOffset(...)
     at org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor.fieldOffset(InstanceFieldAccessors.java:136)
     at org.apache.fory.reflect.InstanceFieldAccessors.createAccessor(InstanceFieldAccessors.java:68)
     at org.apache.fory.reflect.ReflectionUtils.setObjectFieldValue(ReflectionUtils.java:471)
     at org.apache.fory.builder.Generated$GeneratedSerializer$1.onNotifyResult(Generated.java:85)
     at org.apache.fory.builder.JITContext.lambda$registerSerializerJITCallback$0(JITContext.java:96)
 (repeated for .serializer1), immediately followed by:
 java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because the return value of
   "java.util.Map.get(Object)" is null
     at org.apache.fory.builder.JITContext.lambda$registerSerializerJITCallback$0(JITContext.java:95)

The fory-jit-compiler-* threads die, and the generated codec is left with its nested-serializer fields never injected. Only composite types fail; leaf-type codecs (no nested serializer field) compile cleanly even though they too are hidden classes.

Anything Else?

I tried --add-opens=java.base/java.lang.invoke=ALL-UNNAMED does not help (verified). Fory's JDK25+ docs recommend this flag, so I tested with and without it on Zulu 26.28+63. The exception is identical in both cases. That flag governs MethodHandles.Lookup/defineHiddenClass access; it has no effect on Unsafe.objectFieldOffset, which the JVM rejects for any hidden declaring class regardless of module openness. The failing accessor (InstanceFieldAccessors$InstanceAccessor) never consults a Lookup (tt calls Unsafe.objectFieldOffset directly) so this is a code-path defect, not a missing-access/configuration issue.

On JDK 25+, CodecUtils.codecNeighbor returns the bean class for any type whose loader can see Fory, so CodeGenerator.compileAndLoad defines the codec via DefineClass.defineHiddenNestmate (a hidden class). When withAsyncCompilation(true), nested serializers are injected post-construction through Generated$GeneratedSerializer$1.onNotifyResultReflectionUtils.setObjectFieldValueInstanceFieldAccessors.createAccessornew InstanceAccessorUnsafe.objectFieldOffset(field) (InstanceFieldAccessors.java:136), which the JVM rejects for hidden classes. The failing task's finally then runs hasJITResult.clear() while a concurrent successful task is at JITContext.java:95 (for (… : hasJITResult.get(callback.id()))), causing the NPE. Only composite types are affected (leaf codecs have no nested serializer field to inject).

Workaround:
withAsyncCompilation(false) (keeps codegen/JIT) or withCodegen(false) (interpreter only).

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions