Environment Information
$ jruby -v
jruby 9.4.11.0 (3.1.4) 2025-01-29 9b107851a3 OpenJDK 64-Bit Server VM 23.0.2 on 23.0.2 +jit [arm64-darwin]
$ uname -a
Darwin hostname.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:22 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6041 arm64
Expected Behavior
While testing my WeakSet gem, it turned out that JRuby seems to hold on to object references in the current method when they should have been removed already. This prevents the unexpectedly referenced object from being garbage collected.
Generally, I would have expected the following behavior (which works in CRuby/MRI):
# Create an object with a string reference via the variable
string = +"Hello World"
# Remove the object reference, allowing the String to be garbage collected...
string = nil
Afterwards, I would expect that the String object is garbage collected, even if the execution is still contained within the current method. It turns out that this is not the case.
Actual Behavior
Unfortunately, it turns out that on JRuby, the string is only garbage collected after leaving the scope where it was assigned to a variable. The following example shows this by using an ObjectSpace::WeakMap.
Consider the following "setup" for all the three examples below:
begin
require "java"
$jruby = true
rescue LoadError
$jruby = false
end
require "timeout"
def garbage_collect_until(timeout = 5)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
while Process.clock_gettime(Process::CLOCK_MONOTONIC) - started < timeout do
GC.start
if $jruby
Java::JavaLang::System.gc
Java::JavaLang::System.runFinalization
end
return true if yield
end
raise Timeout::Error, "Timeout waiting for garbage collection"
end
With that, the failing base-case for JRuby would be as follows. Note that this succeeds on CRuby/MRI, usually after just one loop in garbage_collect_until. Interestingly, TruffleRuby appears to show the same behavior as JRuby here.
weak_map = ObjectSpace::WeakMap.new
obj = +"Hello World"
weak_map[obj.__id__] = obj
weak_map.size
# => 1
# Remove the object reference
obj = nil
garbage_collect_until { weak_map.size == 0 }
# => Timeout waiting for garbage collection (Timeout::Error)
In contrast, both of the following two example work on JRuby. This appears to indicate that these object references are cleanup up when leaving the (method or thread) scope.
-
Using a thread to set the value
weak_map = ObjectSpace::WeakMap.new
Thread.new do
obj = +"Hello World"
weak_map[obj.__id__] = obj
weak_map.size
end.value
# => 1
weak_map.size
# => 1 (this may not always hold if there was a garbage collect run in-between)
garbage_collect_until { weak_map.size == 0 }
# => true
weak_map.size
# => 0
-
Using a separate method
def add_hello_world(weak_map)
obj = +"Hello World"
weak_map[obj.__id__] = obj
weak_map.size
end
weak_map = ObjectSpace::WeakMap.new
add_hello_world(weak_map)
# => 1
weak_map.size
# => 1 (this may not always hold if there was a garbage collect run in-between)
garbage_collect_until { weak_map.size == 0 }
# => true
weak_map.size
# => 0
By fixing this issue, it may be possible to re-enable the WeakRef tests in
test/mri/excludes/TestWeakRef.rb (and by introducing Java-specific means to force garbage collection in the actual tests in test/mri/test_weakref.rb.
Environment Information
Expected Behavior
While testing my WeakSet gem, it turned out that JRuby seems to hold on to object references in the current method when they should have been removed already. This prevents the unexpectedly referenced object from being garbage collected.
Generally, I would have expected the following behavior (which works in CRuby/MRI):
Afterwards, I would expect that the String object is garbage collected, even if the execution is still contained within the current method. It turns out that this is not the case.
Actual Behavior
Unfortunately, it turns out that on JRuby, the string is only garbage collected after leaving the scope where it was assigned to a variable. The following example shows this by using an
ObjectSpace::WeakMap.Consider the following "setup" for all the three examples below:
With that, the failing base-case for JRuby would be as follows. Note that this succeeds on CRuby/MRI, usually after just one loop in
garbage_collect_until. Interestingly, TruffleRuby appears to show the same behavior as JRuby here.In contrast, both of the following two example work on JRuby. This appears to indicate that these object references are cleanup up when leaving the (method or thread) scope.
Using a thread to set the value
Using a separate method
By fixing this issue, it may be possible to re-enable the WeakRef tests in
test/mri/excludes/TestWeakRef.rb (and by introducing Java-specific means to force garbage collection in the actual tests in test/mri/test_weakref.rb.