Skip to content

[Java] Multiple Fory instances cannot be constructed on Java 8 (Non-ASCII characters in meta string) #4017

Description

@thmd

Search before asking

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

Version

Fory 1.6.1 and 1.7.0 (latest). Fails on Java 8, passes on Java 21.
fory-core declares maven.compiler.source/target = 8, so Java 8 is a supported target.

Failing JDK : 1.8.0_491-b10, HotSpot 64-Bit Server VM 25.491-b10
Passing JDK : Temurin 21.0.11+10-LTS
OS          : macOS 15 (Darwin 24.6.0), arm64

Component(s)

Java

Minimal reproduce step

fory-multi-instance-repro.zip

On Java 8, constructing a second Fory instance in the same JVM fails, once an earlier
instance has serialized an object graph large enough to trigger JIT codegen. The failure comes
from ClassResolver.initialize() while registering Fory's own built-in types — the string
it fails to encode is the package or type name of a Fory internal class, which is ASCII by
construction.

A single Fory instance never fails, at any payload size (checked to ~3.3 MB / 150k objects),
so this is not a serialization-size problem. Once it throws, every later Fory construction in
that JVM fails the same way.

The practical impact is on any application holding more than one Fory — two library modules
that each keep a static instance, or one instance per classloader in a container. The second
can become permanently un-constructible depending on what the first happened to serialize.

The attached fory-multi-instance-repro.zip is a single self-contained file, no dependencies
beyond fory-core:

unzip fory-multi-instance-repro.zip && cd fory-multi-instance-repro
curl -sO https://repo1.maven.org/maven2/org/apache/fory/fory-core/1.7.0/fory-core-1.7.0.jar
javac -cp fory-core-1.7.0.jar ForyMultiInstanceRepro.java
java  -cp fory-core-1.7.0.jar:. ForyMultiInstanceRepro

Optional args: [items] [instances] [builderConfig], defaults 800 4 copy+num.

Some configurations are probabilistic, so run it a few times to see a rate:

for i in $(seq 1 10); do java -cp fory-core-1.7.0.jar:. ForyMultiInstanceRepro; done

What did you expect to see?

OK items=800 instances=4 bytes=194341

What did you see instead?

10 of 10 runs fail on Java 8; 0 of 10 on Java 21.

FAIL at instance 2 of 4 (items=800, bytes=194341): java.lang.IllegalArgumentException: Non-ASCII characters in meta string are not allowed

java.lang.IllegalArgumentException: Non-ASCII characters in meta string are not allowed
	at org.apache.fory.meta.MetaStringEncoder.encodeBinary(MetaStringEncoder.java:92)
	at org.apache.fory.resolver.SharedRegistry.getEncodedMetaString(SharedRegistry.java:337)
	at org.apache.fory.resolver.SharedRegistry.getPackageEncodedMetaString(SharedRegistry.java:309)
	at org.apache.fory.resolver.TypeInfo.<init>(TypeInfo.java:106)
	at org.apache.fory.resolver.ClassResolver.registerInternalImpl(ClassResolver.java:728)
	at org.apache.fory.resolver.ClassResolver.registerInternal(ClassResolver.java:716)
	at org.apache.fory.resolver.ClassResolver.initialize(ClassResolver.java:273)
	at org.apache.fory.Fory.<init>(Fory.java:155)
	at org.apache.fory.config.ForyBuilder.newFory(ForyBuilder.java:789)
	at org.apache.fory.ThreadLocalFory.newFory(ThreadLocalFory.java:67)

Variations across runs of the same command, all with the above root cause:

  - it also fails via getTypeNameEncodedMetaString (SharedRegistry.java:317), so either meta
    string can be the one that fails;
  - on 1.7.0 it is sometimes surfaced wrapped, as
    org.apache.fory.exception.SerializationException: java.lang.RuntimeException: Create sequential serializer failed
    (7/10 raw, 3/10 wrapped in one sample of 10);
  - the failing instance index is usually 2, occasionally 3.

Results, 10 runs per cell:

  fory    items  instances  Java 8         Java 21
  1.6.1   800    4          10/10 fail     0/10
  1.7.0   800    4          10/10 fail     0/10
  1.6.1   800    1          0/10           0/10

Anything Else?

Conditions, each verified by removing it:

  1. Java 8. Not reproducible on Java 21, including at 8 instances and 4x the payload.
  2. More than one Fory instance constructed in the JVM.
  3. An earlier serialization large enough to trigger JIT codegen. Built-in types alone never
    trigger it. A flat HashMap<Integer, UserType> of 6000 entries does NOT reproduce; several
    distinct generic shapes (Map<Integer,.>, Map<Long,.>, Set) do. It is not a simple
    size threshold — the same shape reproduces at 2000 entries per field but not at 10000.
  4. withRefTracking(true).

The failure rate tracks how many builder methods are called. With withLanguage +
requireClassRegistration(false) + withRefTracking(true) alone it reproduces intermittently;
adding further builder calls raises the rate (8 runs each, Java 8; reproduce each row with the
third arg, e.g. ... ForyMultiInstanceRepro 800 4 min):

builder config arg fail rate
minimal (above) min 2/8

  • withRefCopy(true) copy 2/8
  • serializeEnumByName(true) enum 6/8
  • withNumberCompressed(false) num 7/8
  • withRefCopy(true) + withNumberCompressed(false) copy+num 8/8

serializeEnumByName(true) raises the rate although the payload contains no enums, and
withRefCopy(true) affects only copy(), which is never called here. That suggests the trigger is
sensitive to the number of action-recording builder methods replayed in ForyBuilder.factory(),
rather than to any single option's semantics.

withCodegen(false) and withAsyncCompilation(true) each reduce the rate without eliminating it,
so neither is a workaround.

Ruled out:

  • A shared SharedRegistry instance. Fory.(builder, classLoader, sharedRegistry) does
    new SharedRegistry() when the argument is null, so separate instances do not share one.
  • The isLatin/ASCII range gap. StringEncodingUtils.isLatin(char[]) accepts chars <= 255 while
    the LOWER_SPECIAL encoders reject > 127, but encodeBinary(String, Encoding[]) correctly
    falls back to UTF-8 — verified with "café", "aÿ", "aĀ", "a中" on both JDKs.
    The gap applies only to the forced-encoding overload, which is what this path uses.
  • The multi-release jar. PlatformStringUtils and MemoryBuffer are overridden only under
    META-INF/versions/25/, so Java 8 and Java 21 both load the base classes.

Possibly related:

Suggested hardening, independent of root cause:
MetaStringEncoder.encodeBinary(String, Encoding) throws when the forced encoding cannot
represent the string, while the Encoding[] overload falls back to UTF-8. Making the
forced-encoding path fall back the same way would downgrade this from an error that permanently
prevents Fory construction in the JVM to correct (if slightly larger) output.

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions