Project

General

Profile

Actions

Bug #22251

closed

ObjectSpace::WeakMap#size can return a stale value

Bug #22251: ObjectSpace::WeakMap#size can return a stale value

Added by Eregon (Benoit Daloze) 23 days ago. Updated 21 days ago.

Status:
Closed
Assignee:
-
Target version:
-
ruby -v:
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin25]
[ruby-core:126434]

Description

$ ruby -e 'GC.disable; m = ObjectSpace::WeakMap.new; Thread.new { m[Object.new] = Object.new }.join; GC.start; p m.size; p m.keys; p m.size' 
1
[]
0

The first line is 1 but I would expect 0.
Interestingly calling #keys seems to force an update.

If I GC twice then it seems correct:

$ ruby -e 'GC.disable; m = ObjectSpace::WeakMap.new; Thread.new { m[Object.new] = Object.new }.join; GC.start; GC.start; p m.size; p m.keys; p m.size' 
0
[]
0

Could we decrease size whenever we "remove" an entry?

Apologies if that's already known, I searched for an existing issue for this but didn't find anything.

Updated by nobu (Nobuyoshi Nakada) 23 days ago Actions #1

master b52af71445 works as expected.

Updated by headius (Charles Nutter) 23 days ago Actions #2 [ruby-core:126437]

The implementation of WeakMap on JRuby probably behaves the same, because it uses weak references that are only cleaned along certain code paths.

As you (@Eregon (Benoit Daloze)) know, the JVM GC will vacate weak references, but handling the now-dead reference object is up to user code.

That may be done lazily, via "cleaner" code in other map operations. In this example, the keys method may be triggering such a cleanup, and doing the same in size would fix your issue at the cost of always polling for dead slots.

It can also be done actively, via a separate thread that waits for references to be vacated. In that case, you're waiting for the GC, thread scheduler, and various memory caches to finish cleanup and propagate the results.

In both strategies, there's few guarantees that the remaining references within the map are actually occupied.

In your example, having size run clean-up logic might fix the issue, but I would strongly recommend not requiring that such methods deterministically reflect the actual occupation of the map. Better to explicitly say that the size only reflects recent occupation of the map, and may be inaccurate until all GC-related cleanup has happened.

I'd also point out that on other runtimes like the JVM, forcing a single GC may not fully vacate a weak reference, since there may be complicated chains of references to resolve or finalization/cleaner logic yet to run. The JVM spec makes no guarantees about when objects are collected, which also means there's no guarantees about when weak references are cleared. Specifying behavior like you want across all Ruby implementations would be very problematic.

Updated by Eregon (Benoit Daloze) 21 days ago Actions #3 [ruby-core:126450]

Both Java's WeakHashMap and JRuby's variant clean references in size(), so there is no issue there:
https://github.com/openjdk/jdk/blob/05eaf783b270ec5cd901704c5e16b6201de7a9e4/src/java.base/share/classes/java/util/WeakHashMap.java#L391
https://github.com/jruby/jruby/blob/4d041a906d427b7fee577b9a2938e2fcda6c5522/core/src/main/java/org/jruby/util/collections/WeakValuedMap.java#L74

I'm well aware it's tricky to GC an object reliably, but that's not the point of this issue, it's about consistency in what gets reported.

Updated by Eregon (Benoit Daloze) 21 days ago Actions #4 [ruby-core:126451]

  • Status changed from Open to Closed
  • Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: WONTFIX, 3.4: REQUIRED, 4.0: REQUIRED

My reproducer isn't prefect because the 1; []; 0 output would be correct if a GC happens at the start of m.keys, but we can disable the GC to resolve that concern and make it fully reliable.

nobu (Nobuyoshi Nakada) wrote in #note-1:

master b52af71445 works as expected.

Indeed, I should have checked, I'll close this and mark for backport then, both 3.4 and 4.0 seem affected, in fact since ruby-3.3.0-preview2:

$ ruby -ve 'GC.disable; m = ObjectSpace::WeakMap.new; Thread.new { m[Object.new] = Object.new }.join; GC.start; p m.size; p m.keys; p m.size'
ruby 4.1.0dev (2026-08-08T08:04:35Z master e56f452e46) +PRISM [x86_64-linux]
0
[]
0

$ docker run -it -e ALL_RUBY_SINCE=3.0.0 ghcr.io/ruby/all-ruby /all-ruby/all-ruby -e 'GC.disable; m = ObjectSpace::WeakMap.new; Thread.new { m[Object.new] = Object.new }.join; GC.start; p m.size; p m.keys; p m.size'
ruby-3.0.0          0
                    []
                    0
...
ruby-3.3.0-preview1 0
                    []
                    0
ruby-3.3.0-preview2 1
                    []
                    0
...
ruby-4.0.6          1
                    []
                    0

Updated by Eregon (Benoit Daloze) 21 days ago Actions #5

  • Description updated (diff)

Updated by Eregon (Benoit Daloze) 21 days ago ยท Edited Actions #6 [ruby-core:126452]

  • Backport changed from 3.3: WONTFIX, 3.4: REQUIRED, 4.0: REQUIRED to 3.3: WONTFIX, 3.4: WONTFIX, 4.0: WONTFIX

I bisected it to https://github.com/ruby/ruby/commit/b2feb09efe.
This seems big to backport, and I agree it's not so important, so marking all versions as WONTFIX.

#size is just st_table_size(w->table) so what changed is that purging entries was lazy before that commit and eager after (rb_wmap_handle_weak_references).
The behavior on master is better so I'll make a PR to add a CRuby test for it so we don't regress this unknowingly: https://github.com/ruby/ruby/pull/18407

Updated by headius (Charles Nutter) 21 days ago Actions #7 [ruby-core:126455]

Both Java's WeakHashMap and JRuby's variant clean references in size(), so there is no issue there:

But your "failing" example only runs a single GC, which may not always be sufficient to clear the reference, in which case the size would not decrease. Ergo it's not deterministic and shouldn't be relied upon.

Updated by Eregon (Benoit Daloze) 21 days ago Actions #8 [ruby-core:126456]

headius (Charles Nutter) wrote in #note-7:

But your "failing" example only runs a single GC, which may not always be sufficient to clear the reference, in which case the size would not decrease. Ergo it's not deterministic and shouldn't be relied upon.

It is deterministic on CRuby (m.keys is always empty after a single GC), and this issue is about CRuby's behavior.

(yes this pattern doesn't reliably GC the object on JRuby & TruffleRuby)

Actions

Also available in: PDF Atom