Project

General

Profile

Actions

Bug #22251

open

ObjectSpace::WeakMap#size can return a stale value

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

Added by Eregon (Benoit Daloze) about 9 hours ago. Updated about 3 hours ago.

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

Description

$ ruby -e '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 '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) about 6 hours ago Actions #1

master b52af71445 works as expected.

Updated by headius (Charles Nutter) about 3 hours 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.

Actions

Also available in: PDF Atom