In Ruby, what does ObjectSpace.each_object(Session) yield and return, and why is its count not an exact number of live sessions?
answer
- walks the whole heap
- subclasses match too
- returns an Integer count
- unswept garbage still counted
- multi-Ractor mode skips some
basics
~20 sIt yields every heap object that is a Session or subclass instance and returns how many it yielded. The count can include unreachable objects the GC has not yet collected, so it is an upper bound, not a live count.
solid answer
~40 s`ObjectSpace.each_object(Session) { |s| ... }` walks the object heap and yields every non-immediate object for which `is_a?(Session)` is true, so subclass instances match too; with a block it returns the number yielded, and without one it returns an `Enumerator`. It scans heap slots, not references, so an object that is already unreachable but not yet swept by the garbage collector is still yielded, and the count is an upper bound; running a full collection first narrows it but guarantees nothing. Immediates such as small Integers, static Symbols, `true`, `false` and `nil` never appear. Once `Ractor.new` has been called, it skips Ractor-unshareable objects. The walk touches the whole heap, so it is a console and diagnostics tool, not something to run per request.
code
ruby · 11 linesclass Session; end
class AdminSession < Session; end
keep = [Session.new, AdminSession.new]
3.times { Session.new } # unreachable at once
ObjectSpace.each_object(Session).count # => up to 5: unswept garbage included
GC.start
ObjectSpace.each_object(Session).count # => usually 2 after a full collection
ObjectSpace.each_object(Session) { |s| p s } # prints each; returns the count
ObjectSpace.each_object(Integer).count # small Integers are immediates, never yieldedgo deeper
Recall that ObjectSpace.each_object(SomeClass) visits objects of that class in memory and returns how many it found.
Explain why unswept garbage is counted, why immediates are never yielded, and that subclass instances match the filter.
Use each_object only as a console or diagnostic probe, read its count as an upper bound, and prefer registries or profiling tools in real systems.
Steer teams toward observable instance tracking and proper memory tooling rather than heap walks when leaks become a recurring question.
## What each_object does `ObjectSpace.each_object` iterates over the objects the Ruby process currently holds in its heap. With a class or module argument it filters them: - **With a block**, it yields each matching object and returns an `Integer`: how many it yielded. - **Without a block**, it returns an `Enumerator`, so `ObjectSpace.each_object(Session).count` or `.to_a` work too. - The filter uses kind-of matching: instances of `Session` **and of its subclasses** are yielded, and so is any object whose class includes a module you pass. The typical uses are diagnostic: "how many `Session` objects exist right now?" in a console, finding a stray object, or confirming a suspected leak. ## Why the count is not exact The method walks **heap slots** and yields every valid object it finds. It does not check whether anything still refers to the object. So: 1. **Garbage not yet collected is included.** An object that became unreachable a moment ago still occupies its slot until the collector sweeps it, and `each_object` yields it. The count is an upper bound on live objects. 2. **Running a collection first helps but guarantees nothing.** A full collection before counting removes most garbage, but an object still referenced from a temporary, or from the machine stack that the collector scans conservatively, can survive. 3. **Immediates never appear.** Values such as small Integers, static Symbols, `true`, `false` and `nil` are not heap objects, so `each_object(Integer)` does not see them. 4. **Multi-Ractor mode skips objects.** The documentation notes that once `Ractor.new` has been called, `each_object` yields only Ractor-shareable objects, so an unfrozen `Hash`, for example, disappears from the walk. | Situation | Effect on the count | |---|---| | unreachable but unswept objects | counted | | subclass instances | counted | | immediate values | never counted | | unshareable objects after `Ractor.new` | not counted | ## Cost The walk visits every heap slot, which in a large application means millions of objects, and it runs your block for each match. It is fine in a console or a one-off diagnostic script and wrong in a request path or a periodic job that runs often. ## Using it to confirm a suspected leak A count is most useful as a **trend**, not a single number: 1. Count the suspect class after a full collection. 2. Run the workload you suspect, for example a batch of requests in a test process. 3. Collect again and count again. 4. Repeat several cycles. A count that returns to the same level each cycle is normal. A count that climbs by roughly the same amount every cycle means something keeps those objects reachable. At that point `each_object` has done its job: the next step is finding *who* holds the references, which is work for heap-dump and allocation tools rather than for iteration. ## Better tools for common goals - **Finding subclasses of a class**: modern Ruby exposes them directly on the class, which is cheaper and does not depend on heap state. - **Tracking instances you create**: keep an explicit registry, or count in the constructor and in the code that retires objects. - **Memory analysis**: allocation and heap statistics tools answer "what is using memory?" better than iterating objects by hand. ## Mistakes to avoid - Reading the count as the exact number of live objects. - Expecting `each_object(Integer)` to find small integers. - Using `each_object` in production code paths as a registry. - Forgetting that subclass instances are included when you pass a base class. - Assuming the result is the same after the process has created a Ractor.
- Why does ObjectSpace.each_object(Hash) find fewer objects after the program calls Ractor.new?Its documentation notes that in multi-Ractor mode, which starts with the first `Ractor.new`, `each_object` yields only Ractor-shareable objects. An ordinary unfrozen `Hash` is not shareable, so it drops out of the walk even though it still exists.
- What does each_object return when called without a block?An `Enumerator` over the same walk, so you can chain `count`, `select` or `to_a`. With a block it returns an `Integer`, the number of objects it yielded.
saying these in an interview costs you the question
- each_object counts only objects that are still reachable
- each_object(Session) excludes instances of subclasses
- each_object(Integer) lists every integer in use
- Calling GC.start first makes the count exact
- each_object is cheap enough to call on every request