skip to content

In Ruby, what does ObjectSpace.each_object(Session) yield and return, and why is its count not an exact number of live sessions?

level: seniorimportance: nice to knowfreq 15%

answer

  1. walks the whole heap
  2. subclasses match too
  3. returns an Integer count
  4. unswept garbage still counted
  5. multi-Ractor mode skips some

basics

~20 s

It 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 lines
ruby
class 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 yielded

go deeper

for a junior

Recall that ObjectSpace.each_object(SomeClass) visits objects of that class in memory and returns how many it found.

for a middle

Explain why unswept garbage is counted, why immediates are never yielded, and that subclass instances match the filter.

for a senior

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.

for a principal

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