Understanding JavaScript Garbage Collection behavior in memory intensive tasks

59 Views Asked by At

I am currently investigating memory usage patterns related to JavaScript's garbage collection in an application I am developing. I have constructed a simple benchmark to better understand GC's behavior. It includes two separate tasks (Task A and Task B) as shown below.

  const button = document.getElementById('begin-test');
  button.onclick = () => {
    console.log('start');

    const LOOPS = 40000000;
    let array = [];

    // Task A
    for (let index = 0; index < LOOPS; index++) {
      const value = [{ value: index }];
      array.push(value[0].value);
    }

    // Task B
     for (let index = 0; index < LOOPS; index++) {
       array.push(index);
    }

    array = null;

    console.log('done');
  }
}

Task A and Task B are executed separately, as I alternate commenting one of them during my tests. No more Javascript is executed.

The application starts with an approximate memory usage of 3MB. Post the execution of Task A, Chrome's Memory inspection tool indicates a memory usage of 230MB. Forcing a garbage collection brings it back down to 3MB. However, after executing Task B, the memory usage is reported as 580MB(!). Forcing a garbage collection again reduces memory usage back to around 3MB. This behavior is consistent across fresh tabs for each test.

I have three questions regarding this behavior:

  1. Is the explicit memory allocation in Task A responsible for its lower "memory wastage"?
  2. Why doesn't setting array = null immediately trigger garbage collection? (both array = [] and array.length = 0 yield the same results)
  3. Could the simplicity of my test be affecting its accuracy in this specific case?
1

There are 1 best solutions below

2
Alexander Nenashev On

GC doesn't free resources as soon as they are dereferenced. Don't rely on assumption that

  1. When you allocate memory the JS heap memory should be increased (often it's decreased, since after allocating the memory GC often frees the previous allocated dereferences resources)
  2. When you dereference a resource GC frees its memory (no, GC frees memory when it decides it's more appropriate).

let prevMemory;

const registry = new FinalizationRegistry(obj => {
  console.log(obj, 'released');
});
let callId = 0;
async function test(){
  const id = ++callId;
  
  console.log('start ' + id);
  prevMemory = performance.memory.usedJSHeapSize;
  
  await new Promise(r => requestAnimationFrame(r));

  const LOOPS = 40000000;
  let array = [];
  registry.register(array, `array ${id}`);

  // Task A
  for (let index = 0; index < LOOPS; index++) {
    const value = [{ value: index }];
    array.push(value[0].value);
  }

  // Task B
   for (let index = 0; index < LOOPS; index++) {
     array.push(index);
  }

  array = null;

  console.log('done ' + id);
  console.log('allocated bytes:', performance.memory.usedJSHeapSize - prevMemory);
  prevMemory = performance.memory.usedJSHeapSize;
  
}
button{
  padding: 5px 30px;
  border-radius:5px;
  box-shadow: 0 0 10px rgba(0,0,0,.2);
  cursor:pointer;
}
<button onclick="test()">test</button>