Skip to content

Block scoped variables are not getting garbage collected and lead to OOM crash #59442

@JelleRoets-Work

Description

@JelleRoets-Work

Version

v22.18.0

Platform

Darwin MacBookPro 24.6.0 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:29 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T6000 arm64

Subsystem

No response

What steps will reproduce the bug?

run with 32 Mb of memory: node --max-old-space-size=32 test.js

// test.js
const registry = new FinalizationRegistry(name => console.log(`${name} garbage collected`));

class BigObject {
  constructor(name) {
    this.name = name;
    this.data = Array(1e6).fill(1); // Allocate 8 Mb of data
    console.log(`${name} created`);
    registry.register(this, name);
  }
  [Symbol.dispose]() {
    console.log(`${this.name} out of scope`); // Only works from V8 v13.8 https://v8.dev/features/explicit-resource-management
  }
}

{
  const obj = new BigObject('obj1');
}
{
  const obj = new BigObject('obj2');
}
{
  const obj = new BigObject('obj3');
}
{
  const obj = new BigObject('obj4');
}
{
  const obj = new BigObject('obj5');
}

Results in a (unexpected) OOM crash

How often does it reproduce? Is there a required condition?

always

What is the expected behavior? Why is that the expected behavior?

Since each obj goes out of scope after exiting its surrounding block scope, only 1 big object (of 8
Mb) is in scope at the same time, which should always fit in the given working memory of 32 Mb, regardless of how many sequential block scopes are defined.

So at least starting from allocating the 5th object, I would expect the garbage collector to start freeing up one or more out of scope object to be able to continue the script:

obj1 created
obj2 created
obj3 created
obj4 created
obj5 created
obj1 garbage collected
// optionally:
obj2 garbage collected
obj3 garbage collected
obj4 garbage collected
obj5 garbage collected

What do you see instead?

obj1 created
obj2 created
obj3 created

<--- Last few GCs --->

[77687:0x140008000]       39 ms: Scavenge (interleaved) 27.8 (32.5) -> 27.8 (36.5) MB, pooled: 0 MB, 0.62 / 0.00 ms  (average mu = 1.000, current mu = 1.000) allocation failure; 
[77687:0x140008000]       43 ms: Mark-Compact 35.4 (44.2) -> 35.0 (52.2) MB, pooled: 0 MB, 3.00 / 0.00 ms  (+ 0.1 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 9 ms) (average mu = 0.927, current mu = 0.927) fin

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0x1030c225c node::OOMErrorHandler(char const*, v8::OOMDetails const&) [.nvm/versions/node/v22.18.0/bin/node]
 2: 0x1032984fc v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [.nvm/versions/node/v22.18.0/bin/node]

Additional information

Surprisingly function scopes behave differently than block scopes:
If you replace the above script by

(() => {
  const obj = new BigObject('obj1');
})();
(() => {
  const obj = new BigObject('obj2');
})();
(() => {
  const obj = new BigObject('obj3');
})();
(() => {
  const obj = new BigObject('obj4');
})();
(() => {
  const obj = new BigObject('obj5');
})();

we don't face OOM crashes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions