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.
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.
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
endFollow-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.
// 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 useRETENTION 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
Every queue that grows must have a matching shrink path (pop on match, delete on timeout).
Bound mailboxes between producer and consumer — backpressure turns silent leaks into visible stalls.
Null out long-lived debug references (last_txn, error_snapshot) once consumed.
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.