Part 2 · OOP for Verification · Intermediate

Object Lifetime & Memory

When objects become collectable, accidental retention through queues and mailboxes, and the handle-reassignment interview question.

Reachability — the rule that decides everything

An object stays alive exactly as long as it is reachable : some chain of references — a named handle, an element of a queue or associative array, a slot in a mailbox, a property of another live object, a handle captured by a still-running task — leads to it. The moment the last such reference disappears, the object is collectable, and the simulator's garbage collector reclaims it at its own convenience. You never free objects manually; there is no delete for class objects. The practical skill is therefore not freeing memory but recognizing every place a reference can hide.

diagram
REACHABILITY — who keeps an object alive?

  named handle ────────────┐
  queue element ───────────┤
  assoc-array value ───────┼────►  OBJECT  (alive while ANY arrow exists)
  mailbox slot ────────────┤
  property of live object ─┤
  handle held by running ──┘
  task/fork process

  All arrows gone    unreachable    collected automatically.
  ONE forgotten arrow (e.g. a debug queue)  object lives forever.

The interview question: what happens on handle reassignment?

Q: a handle points at object A; you assign it to object B. What happens to A? The precise answer: nothing happens to A immediately. The handle now references B. If that handle was the last reference to A, A becomes unreachable and will be garbage-collected; if any other reference to A exists — another handle, a queue entry, a mailbox — A lives on, fully intact. Reassignment never destroys an object; it only drops one reference.

systemverilog
bus_txn h;
h = new();            // object A created, 1 reference
h.addr = 32'hAAAA;

bus_txn keep = h;     // A now has 2 references

h = new();            // object B created; h drops A, points at B
                      // A still alive — 'keep' reaches it
$display("%0h", keep.addr);   // AAAA — A is intact

keep = null;          // last reference to A dropped
                      // A is now unreachable → collectable

// Loop version — no leak, despite 1000 new() calls:
repeat (1000) begin
  bus_txn t = new();  // each iteration: old object loses its
  void'(t.randomize());//  only reference and becomes collectable
end

Follow-up traps interviewers add

  • 'So does h = null free the object?' — it removes one reference; freeing happens only if that was the last one.

  • 'Can I get a dangling handle?' — no; handles are null or point at live objects, GC never frees reachable memory.

  • 'Is there a destructor?' — no user destructor in SystemVerilog; cleanup hooks must be explicit method calls.


Accidental retention — the testbench memory leak pattern

Garbage collection means no dangling pointers, but it does NOT mean no leaks. A leak in a class-based testbench is logical : objects you will never use again remain reachable, so the collector must keep them. Over a long soak run this grows without bound. The usual suspects are append-only queues and unread mailboxes — every transaction ever seen stays alive because one collection still references it.

systemverilog
// LEAK 1: append-only scoreboard queue
class scoreboard;
  bus_txn seen_q[$];
  function void write_act(bus_txn t);
    seen_q.push_back(t);          // pushed, matched... never popped
    check_against_expected(t);    // forgot: seen_q.pop_front() /
  endfunction                     //         delete matched entry
endclass
// 10M transactions → 10M live objects → sim memory climbs all night

// LEAK 2: mailbox with no consumer
mailbox #(bus_txn) mb = new();    // unbounded
task producer();
  forever begin
    bus_txn t = new();
    void'(t.randomize());
    mb.put(t);                    // nobody ever calls mb.get()
  end                             // every txn retained inside mb
endtask

// FIXES
//  - pop/delete entries when matched:  exp_q.delete(idx);
//  - bound the mailbox:  mailbox #(bus_txn) mb = new(16);
//    (put() now blocks → backpressure exposes the missing consumer)
//  - null out 'last seen' debug handles after use
diagram
RETENTION GROWTH over a soak run

  live
  objects │                                    ┌── append-only queue
          │                              ┌─────┘    (leak: unbounded)
          │                        ┌─────┘
          │                  ┌─────┘
          │            ┌─────┘
          │  ──┬──┬──┬──┬──┬──┬──┬──  ◄── healthy TB: objects created
          │                                and released per transaction
          └────────────────────────────────────────► sim time

  Symptom: memory climbs linearly with transaction count.
  First places to look: scoreboard queues, mailboxes, coverage caches.

Lifetime hygiene checklist

  1. Every queue that grows must have a matching shrink path (pop on match, delete on timeout).

  2. Bound mailboxes between producer and consumer — backpressure turns silent leaks into visible stalls.

  3. Null out long-lived debug references (last_txn, error_snapshot) once consumed.

  4. In end_of_test checks, report nonzero queue/mailbox residue — leftover entries are unmatched transactions AND retention.

Key takeaways

  • Objects live while reachable through any reference chain; collection is automatic when the last reference drops.

  • Reassigning a handle never destroys the old object — it just removes one reference to it.

  • GC prevents dangling handles, not leaks — append-only queues and unread mailboxes retain everything forever.

  • Bound mailboxes and pop matched queue entries — make object release an explicit part of the dataflow design.

Common pitfalls

  • Pushing every observed txn into a debug queue 'just in case' — unbounded retention on long regressions.

  • Unbounded mailbox with a dead consumer thread — memory climbs silently instead of the sim stalling visibly.

  • Assuming h = null frees the object while a scoreboard still holds it — it stays alive, by design.

  • Expecting destructor-style cleanup — SystemVerilog has none; resources need explicit release methods.