Memory management in JavaScript

Eduard Hayrapetyan
Preezma Software Development Company
8 min readJul 19, 2020

Overview

When something gets displayed on your webpage, where is it stored? The process of memory allocation is automatic in modern browsers, since in JavaScript we are not performing any manual memory operation however it is good to know how it works.This helps analyze and debug things, forces us to be more inventive developers also understand the work behind the scenes.Languages, like C have low-level memory management ,and the developer had to allocate memory by using malloc() and free().

Memory life cycle

No matter what programming language you’re using, memory life cycle is pretty much always the same:

  1. Allocate memory
  2. Use memory
  3. Release memory

Here is an overview of what happens at each step of the cycle:

  • Allocate memory — memory is allocated by the operating system which allows your program to use it.
  • Use memory — this is the time when your program actually makes use of the previously allocated memory. Read and write operations are taking place as you’re using the allocated variables in your code.
  • Release memory — now is the time to release the entire memory that you don’t need so that it can become free and available again. As with the Allocate memory operation, this one is explicit in low-level languages.

V8 memory structure 🧠

First, let us see what the memory structure of the V8 engine is. Since JavaScript is single-threaded V8 also uses a single process via JavaScript context. V8 allocates objects in memory into 6 chunks or spaces.The divided chunks also called Resident Set.

  1. New space
  2. Old pointer space
  3. Old data space
  4. Large object space
  5. Code space
  6. Map Space
  • New Space: New space or Young generation is where new allocated objects live and most of these objects are short-lived. This space is small and has two semi-space(1–8 MB). So when you create an object which size fits in 1–8 megabytes, it will go to the new space.
  • Old pointer space: Contains survived objects that have pointers to other objects. Default size is 1.4GB , but you can make it bigger by max-old-space-size v8 Flag.
  • Old data space: Contains objects that just contain data without pointer to other objects. Ascii characters, Strings, numbers, and arrays of unboxed doubles are moved here after surviving in “New space” for two minor GC cycles.
  • Large object space: This is where objects which are larger than the size limits of other spaces live. If objects didn’t fits on the new space, they automatically allocated on this space. Each object gets its own [mmap] region of memory. Large objects are never moved by the garbage collector.
  • Code-space: This is where the Just In Time(JIT) compiler stores compiled code blocks. This is the only space with executable memory (although Codes may be allocated in “Large object space”, and those are executable, too).
  • Map space: These spaces contain Maps, Cells, and PropertyCell respectively. Each of these spaces contains objects which are all the same size and has some constraints on what kind of objects they point to, which simplifies collection.

So this point we allocated objects on memory, they’re in our heap, and now a question?:) How we should know what to clean and how to clean it?🗑️🤔

And the answer is Reachability! Memory is considered garbage when it’s no longer reachable or accessible in your code, and here comes the garbage Collector.

V8 Memory management with Garbage collection

V8 manages the heap memory by garbage collection. In simple terms, it frees the memory used by dead objects, objects that are no longer referenced from the Stack directly or indirectly(via a reference in another object) to make space for new object creation.

There are two stages and three different algorithms used for garbage collection by V8: Minor GC (Scavenger) & Major GC.

Minor GC (Scavenger)

This type of GC keeps the young or new generation space compact and clean. Objects are allocated in new-space, which is fairly small (between 1 and 8 MB, depending on behavior). Allocation in “new space” is very cheap: there is an allocation pointer which we increment whenever we want to reserve space for a new object. When the allocation pointer reaches the end of the new space, minor GC is triggered. This process is called Scavenger and it implements Cheney’s algorithm. It occurs frequently and uses parallel helper threads and is very fast.

Let us look at the minor GC process:

The new space is divided into two equal-sized semi-spaces: to-space and from-space. Most allocations are made in from-space .When from-space fills up the minor GC is triggered.

  1. Let us assume that there are already objects on the “from-space” when we start(Blocks 01 to 06 marked as used memory)
  2. The process creates a new object(07)
  3. V8 tries to get required memory from from-space, but there is no free space in there to accommodate our object and hence V8 triggers minor GC
  4. Minor GC recursively traverses the object graph in “from-space” starting from stack pointers(GC roots) to find objects that are used or alive(Used memory). These objects are moved to a page in the “to-space”. Any objects reference by these objects are also moved to this page in “to-space” and their pointers are updated. This is repeated until all the objects in “from-space” are scanned. By end of this, the “to-space” is automatically compacted reducing fragmentation
  5. Minor GC now empties the “from-space” as any remaining object here is garbage
  6. Minor GC swaps the “to-space” and “from-space”, all the objects are now in “from-space” and the “to-space” is empty
  7. The new object is allocated memory in the “from-space”
  8. Let us assume that some time has passed and there are more objects on the “from-space” now(Blocks 07 to 09 marked as used memory)
  9. The application creates a new object(10)
  10. V8 tries to get required memory from “from-space”, but there is no free space in there to accommodate our object and hence V8 triggers second minor GC
  11. The above process is repeated and any alive objects that survived second minor GC is moved to the “Old space”. First-time survivors are moved to the “to-space” and remaining garbage is cleared from “from-space”
  12. Minor GC swaps the “to-space” and “from-space”, all the objects are now in “from-space” and the “to-space” is empty
  13. The new object is allocated memory in the “from-space”

So we saw how minor GC reclaims space from the young generation and keeps it compact. It is a stop-the-world process but it’s so fast and efficient that it is negligible most of the time. Since this process doesn’t scan objects in the “old space” for any reference in the “new space” it uses a register of all pointers from old space to new space.

Major GC

This type of GC keeps the old generation space compact and clean. This is triggered when V8 decides there is not enough old space, based on a dynamically computed limit, as it gets filled up from minor GC cycles.

The Scavenger algorithm is perfect for small data size but is impractical for large heap, as the old space, as it has memory overhead and hence major GC is done using the Mark-Sweep-Compact algorithm.The following image visualizes the mark and sweep process:

  • The Mark-and-sweep algorithm goes through these 3 steps:
  1. Roots: In general, roots are global variables which get referenced in the code. In JavaScript for example, a global variable that can act as a root is the “window” object. The identical object in Node.js is called “global”. A complete list of all roots gets built by the garbage collector.
  2. The algorithm then inspects all roots and their children and marks them as active (meaning, they are not garbage). Anything that a root cannot reach will be marked as garbage.
  3. Finally, the garbage collector frees all memory pieces that are not marked as active and returns that memory to the OS.
  • Incremental GC: GC is done in multiple incremental steps instead of one.
  • Concurrent marking: Marking is done concurrently using multiple helper threads without affecting the main JavaScript thread. Write barriers are used to keep track of new references between objects that JavaScript creates while the helpers are marking concurrently.
  • Sweeping/compacting: Sweeping and compacting are done in helper threads concurrently without affecting the main JavaScript thread.
  • Lazy sweeping. Lazy sweeping involves delaying the deletion of garbage in pages until the memory is required.

Let us look at the major GC process:

  1. Let us assume that many minor GC cycles have passed and the old space is almost full and V8 decides to trigger a “Major GC”
  2. Major GC recursively traverses the object graph starting from stack pointers to mark objects that are used as alive(Used memory) and remaining objects as garbage(Orphans) in the old space. This is done using multiple concurrent helper threads and each helper follows a pointer. This does not affect the main JS thread.
  3. When concurrent marking is done or if memory limit is reached the GC does a mark finalization step using the main thread. This introduces a small pause-time.
  4. Major GC now marks all dead object’s memory as free using concurrent sweep threads. Parallel compaction tasks are also triggered to move related blocks of memory to the same page to avoid fragmentation. Pointers are updated during these steps.

How much memory is your application using & how often do GC cycles occur in your application?

1.You can investigate heap by making a snapshot in Chrome Dev Tools.

Devtools ➡️ Memory ⬇️ Take snapshot

2. npm i heapdump

Heapdump is a javaScript library for node ,and it allows you to take a snapshot of your heap.

Conclusion

Thanks for reading! I hope this helped you to understand how the browser allocates and clears out memory as it runs applications.

References

Safia Abdalla: The Hitchhiker’s Guide to All Things Memory in Javascript

Memory Management and Garbage Collection in V8

--

--