Expected Behavior
When using Java 25 or Java 26 -XX:AOTCacheOutput, JRuby should retain or rebuild dynamic Ruby proxy methods (like generate_salt) for Java classes loaded from external .jar files.
Actual Behavior
While standard JDK classes (like java.util.logging.Logger) survive AOT caching perfectly, classes loaded from external JARs lose their Ruby proxy methods.
Upon loading the AOT cache, JRuby successfully instantiates the Java object from the JAR, but attempting to call a Ruby-fied method (like generate_salt) throws a NoMethodError. Using java_send(:generate_salt, ...) works, proving the class is in the cache but JRuby's dynamic proxy generation was bypassed.
This completely breaks gems like bcrypt in production when utilizing Leyden.
Minimal Reproducible Example
# reproduce.rb
require 'bcrypt'
puts "Salt: #{BCrypt::Engine.generate_salt}"
Step 1: The Training Run (Works)
$ jruby --nocache -J-XX:AOTCacheOutput=test.aot reproduce.rb
Salt: $2a$12$5JAd646raDMwuCpJ8Id0vu
Temporary AOTConfiguration recorded: test.aot.config
Launching child process /usr/lib/jvm/java-26-amazon-corretto/bin/java to assemble AOT cache test.aot using configuration test.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=: --add-opens=java.base/java.io=org.jruby.dist --add-opens=java.base/java.nio.channels=org.jruby.dist --add-opens=java.base/sun.nio.ch=org.jruby.dist --add-opens=java.management/sun.management=org.jruby.dist -Xss2048k -Djffi.boot.library.path=/opt/jruby/lib/jni -Djava.security.egd=file:/dev/urandom --enable-native-access=org.jruby.dist --sun-misc-unsafe-memory-access=allow --module-path=/opt/jruby/lib/jruby.jar -Djruby.home=/opt/jruby -Djruby.lib=/opt/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh -XX:AOTCacheOutput=test.aot -XX:AOTConfiguration=test.aot.config -XX:AOTMode=create
Reading AOTConfiguration test.aot.config and writing AOTCache test.aot
AOTCache creation is complete: test.aot 58662912 bytes
Removed temporary AOT configuration file test.aot.config
Step 2: The AOT Run (Crashes)
$ jruby --nocache -J-XX:AOTCache=test.aot reproduce.rb
NoMethodError: undefined method 'gensalt' for class Java::Bcrypt_jruby::BCrypt
generate_salt at /usr/local/bundle/gems/bcrypt-3.1.21-java/lib/bcrypt/engine.rb:88
<main> at reproduce.rb:4
Working Example
# reproduce.rb
require 'java'
puts "Instantiating Java class..."
logger = java.util.logging.Logger.getLogger("test")
puts "Calling Ruby-fied setter (logger.level = ...)"
# This maps to java.util.logging.Logger#setLevel()
logger.level = java.util.logging.Level::INFO
puts "Success! Level is #{logger.level}"
Step 1: The Training Run (Works)
$ jruby --nocache -J-XX:AOTCacheOutput=test.aot reproduce.rb
Instantiating Java class...
Calling Ruby-fied setter (logger.level = ...)
Success! Level is INFO
Temporary AOTConfiguration recorded: test.aot.config
Launching child process /usr/lib/jvm/java-26-amazon-corretto/bin/java to assemble AOT cache test.aot using configuration test.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djava.class.path=: --add-opens=java.base/java.io=org.jruby.dist --add-opens=java.base/java.nio.channels=org.jruby.dist --add-opens=java.base/sun.nio.ch=org.jruby.dist --add-opens=java.management/sun.management=org.jruby.dist -Xss2048k -Djffi.boot.library.path=/opt/jruby/lib/jni -Djava.security.egd=file:/dev/urandom --enable-native-access=org.jruby.dist --sun-misc-unsafe-memory-access=allow --module-path=/opt/jruby/lib/jruby.jar -Djruby.home=/opt/jruby -Djruby.lib=/opt/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh -XX:AOTCacheOutput=test.aot -XX:AOTConfiguration=test.aot.config -XX:AOTMode=create
Reading AOTConfiguration test.aot.config and writing AOTCache test.aot
AOTCache creation is complete: test.aot 57298944 bytes
Removed temporary AOT configuration file test.aot.config
Step 2: The AOT Run (Works)
$ jruby --nocache -J-XX:AOTCache=test.aot reproduce.rb
Instantiating Java class...
Calling Ruby-fied setter (logger.level = ...)
Success! Level is INFO
Environment
- JRuby version:
jruby 10.0.3.0 (3.4.5) 2026-02-02 b0be2ab713 OpenJDK 64-Bit Server VM 26+35-FR on 26+35-FR +indy +jit [x86_64-linux]
- OS:
Linux 7a78b71edad9 5.15.153.1-microsoft-standard-WSL2 #1 SMP Fri Mar 29 23:14:13 UTC 2024 x86_64 Linux
Expected Behavior
When using Java 25 or Java 26
-XX:AOTCacheOutput, JRuby should retain or rebuild dynamic Ruby proxy methods (likegenerate_salt) for Java classes loaded from external.jarfiles.Actual Behavior
While standard JDK classes (like
java.util.logging.Logger) survive AOT caching perfectly, classes loaded from external JARs lose their Ruby proxy methods.Upon loading the AOT cache, JRuby successfully instantiates the Java object from the JAR, but attempting to call a Ruby-fied method (like
generate_salt) throws aNoMethodError. Usingjava_send(:generate_salt, ...)works, proving the class is in the cache but JRuby's dynamic proxy generation was bypassed.This completely breaks gems like
bcryptin production when utilizing Leyden.Minimal Reproducible Example
Step 1: The Training Run (Works)
Step 2: The AOT Run (Crashes)
Working Example
Step 1: The Training Run (Works)
Step 2: The AOT Run (Works)
Environment
jruby 10.0.3.0 (3.4.5) 2026-02-02 b0be2ab713 OpenJDK 64-Bit Server VM 26+35-FR on 26+35-FR +indy +jit [x86_64-linux]Linux 7a78b71edad9 5.15.153.1-microsoft-standard-WSL2 #1 SMP Fri Mar 29 23:14:13 UTC 2024 x86_64 Linux