--max-semi-space-size vs --max-old-space-size - node.js

What is the the difference between
--max-old-space-size vs --max-semi-space-size in the Nodejs docs?
I understand that they are related to V8 garbage collector, but there doesn't seem to be any clear explanation within the docs.

Garbage collection is the process of reclaiming the memory occupied by
objects that are no longer in use by the application. Usually, memory
allocation is cheap while it’s expensive to collect when the memory
pool is exhausted.
An object is a candidate for garbage collection when it is unreachable
from the root node, so not referenced by the root object or any other
active objects. Root objects can be global objects, DOM elements or
local variables.
The heap has two main segments, the New Space and the Old
Space. The New Space is where new allocations are happening; it
is fast to collect garbage here and has a size of ~1-8MBs. Objects
living in the New Space are called Young Generation. The Old Space
where the objects that survived the collector in the New Space are
promoted into – they are called the Old Generation. Allocation in the
Old Space is fast, however collection is expensive so it is
infrequently performed .
--max-old-space-size is related to the Old Space limit whereas max-semi-space-size is related to the New Space.
New Space has a smaller buffer (as the GC runs more frequently) whereas Old Space is larger and runs less often.
source: risingstack garbage collection.
source: hunting a ghost

Related

What's the difference between Buffer.allocUnsafe() and Buffer.allocUnsafeSlow() in NodeJS?

I've read the docs on the subject.
It says:
When using Buffer.allocUnsafe() to allocate new Buffer instances,
allocations under 4KB are sliced from a single pre-allocated Buffer.
This allows applications to avoid the garbage collection overhead of
creating many individually allocated Buffer instances. This approach
improves both performance and memory usage by eliminating the need to
track and clean up as many individual ArrayBuffer objects.
However, in the case where a developer may need to retain a small
chunk of memory from a pool for an indeterminate amount of time, it
may be appropriate to create an un-pooled Buffer instance using
Buffer.allocUnsafeSlow() and then copying out the relevant bits.
Also I've found this explanation:
Buffer.allocUnsafeSlow is different from Buffer.allocUnsafe() method. In
allocUnsafe() method, if buffer size is less than 4KB than it
automatically cut out the required buffer from a pre-allocated buffer
i.e. it does not initialize a new buffer. It saves memory by not
allocating many small Buffer instances. But if developer need to hold
on some amount of overhead memory for intermediate amount of time,
than allocUnsafeSlow() method can be used.
Though it is hard to understand for me. May you explain it more eloquent and detailed with some examples for both, please?

What happens to allocated pages that are mostly empty?

If a process initially has a number of pages allocated to it in the heap, but a lot of the data in the pages has been deallocated, is there some sort of optimization that the OS does to consolidate the data into one page so that the other pages can be freed?
In general, nothing happens, the heap will continue to have "holes" in it.
Since the (virtual) memory addresses known by a process must remain valid, the operating system cannot perform "heap compaction" on its own. However, some runtimes like .Net do it.
If you are using C or C++, all you can hope for by default is that malloc() will be able to reuse previously deallocated chunks. But if your usage pattern is "allocate a lot of small objects then deallocate half of them at random," the memory utilization will probably not decrease much from the peak.
If a process initially has a number of pages allocated to it in the heap
A process will not initially have pages allocates in a heap.
is there some sort of optimization that the OS does to consolidate the data into one page so that the other pages can be freed
The operating system has no knowledge of user heaps. It allocates pages to the process. What that process does with those pages is up to it (i.e., use them for a heap, stack, code, etc.).
A process's heap manager can consolidate freed chunks of memory. When this occurs, it is normally done to fight heap fragmentation. However, I have never seen a heap manager on a paging system that unmaps pages once they are mapped by the operating system.
The heap of a process never has holes on it. The heap is part of the data segment allocated to a process, that grows dynamically upwards to the top of the stack segment, basically with the use of the sbrk(2) system call (that fixes a new size to the data segment) so the heap is a continuous segment (at least in terms of virtual address space) of allocated pages. malloc(3) never returns the heap space (or part of it) to the system. See malloc(3) for info about this. While there are memory allocators that allow a process to have several heaps (by means of allocating new memory segments, by use of the mmap(2) system call) the segments allocated by a memory allocator are commonly never returned back to the system.
What happens is that the memory allocator reuses the heap space allocated with sbrk(2) and mmap(2) and manages memory for being reused, but it is never returned back to the system.
But don't fear, as this is handled in a good and profitable way by the system, anyway.
That should not affect the overall system management, except from the fact that it consumes virtual address space, and probably page contents will end in the swap device if you don't use them until the process references them again and makes the system to reload them from the swap device(s). If your process doesn't reuse the holes it creates in the heap, the most probable destination is for the system to move them to the swap device and continue reusing it for other processes.
At this moment, I don't know if the system optimices swap allocation by not swapping out zeroed pages, as it does, for example, with text segments of executables (they never go to a swap device, because their contents are already swapped off in the executable file ---this was the reason you couldn't erase in ancient unices a program executable, or the reason there's not need anymore to use the sticky bit in frequently used programs---) but I think it doesn't (and the reason is that it's most improbable the unused pages will be zeroed by the application)
Be warned only in the case you have a 15Gb single process' heap use in your system and 90% of heap use is not in use most of the time. But think better in optimising the allocation resources because a process that consumes 15Gb of heap while most of the time 90%+ is unused, seems to be a poor design. If you have no other chance, simply provide enough swap space to your system to afford that.

How object allocated in the heap measuring time?

When I read the book 《The Garbage Collection HandBook》, the chapter 9
impile that:"object lifetimes are better measured by the number of bytes of heap space allocated between their birth and death.". I am not very understand this sentence. why lifetime can be measured by the allocated bytes? I try to google for that, but I get no answer.
Who can explain that to me? Thanks!
By measuring object lifetimes in terms of bytes allocated between instantiation and death, it is easier for the GC algorithm to adapt to program behaviour.
If the rate of object allocation is very slow, a simple time measurement would show long pauses between collections, which would appear to be good. However, if the byte allocation measurement of object lifetimes is high objects may be getting promoted to a survivor space or the old generation too quickly. By measuring the byte allocation the collector could optimise heap sizes more efficiently by expanding the young generation to increase the number of objects that become garbage before a minor collection occurs. Just using time as this measure would not make the need for the heap resizing obvious.
As the book points out, with multi-threaded applications it is hard to measure byte allocation for individual threads so collectors tend to measure lifetimes in terms of how many collections an object survives. This is a simpler number to monitor and requires less space to record.
“time” is only a scale that allows to bring an order to events. There are many possible units, even in the real world. Inside the computer, for the purpose of garbage collection, there is no real world’s time unit needed, all the garbage collector usually wants to know, is, which object is older than the other.
For this purpose, just assigning an ascending number to each allocated object would be sufficient, but this would imply maintaining an additional counter. In contrast, the number of allocated bytes comes for free. It’s important that we accumulate the allocated bytes only, never subtracting deallocated bytes, so we have an always growing number.
In a generational memory management, this number doesn’t need to be updated on every allocation, as objects are allocated continuously in a dedicated space, so their addresses represent their relative age within this memory region whereas the start of the region is associated with the last garbage collection. Only when the garbage collector runs and moves the surviving objects, it has to merge this information into an absolute age, if needed.
Implementations like the HotSpot JVM simplify this further. For surviving objects, it maintains a small counter holding the number of garbage collection cycles it survived. After having survived a configurable number of collection cycles, it gets promoted to the old generation and beyond that point, the object’s age becomes irrelevant.

Node.js buffer garbage collection

buf.slice([start[, end]])
Returns a new Buffer that references the same memory as the original, but offset and cropped by the start and end indices.
Note that modifying the new Buffer slice will modify the memory in the original Buffer because the allocated memory of the two objects overlap.
How does garbage collector handle allocated memory if one of the references is gone?
When you perform a slice on a Buffer, you are only creating a new reference to the original buffer, which starts and ends at different points.
If you change the original buffer, the sliced reference will also change.
What this means is that the entire chunk of memory won't be available for garbage collection until all references (sliced or not) are gone.
Hope this answers your question.
From the Node.js buffer documentation: "The implementation of Buffer#slice() creates a view over the existing Buffer without copying, making Buffer#slice() far more efficient.". This means that the buffers reference the same memory location resulting in the overlap. Only once all references to the buffer have been removed, can the memory be reallocated by the garbage collector (gc). When the gc runs, it will delete the buffers that have no references and return the memory to the respective pools.
Node buffers vary in behavior depending on how you initialized them. If you used the new Buffer() methods, they are now deprecated and should revisit the docs. You should use the buffer alloc(), bufferUnsafe() and bufferUnsafeSlow() methods.

Does the GHC garbage collector have any special optimisations for large objects?

Does the GHC garbage collector handle "large" objects specially? Or does it treat them exactly the same as any other object?
Some GC engines put large objects in a separate area, which gets scanned less regularly and possibly has a different collection algorithm (e.g., compacting instead of copying, or maybe even using freelists rather than attempting to defragment). Does GHC do anything like this?
Yes. The GHC heap is not kept in one contiguous stretch of memory; rather, it is organized into blocks.
When an allocated object’s size is above a specific threshold (block_size*8/10, where block_size is 4k, so roughly 3.2k), the block holding the object is marked as large (BF_LARGE). Now, when garbage collection occurs, rather than copy large objects from this block to a new one, the block itself is added to the new generation's set of blocks; this involves fiddling with a linked list (a large object list, to be precise).
Since this means that it may take a while for us to reclaim dead space inside a large block, it does mean that large objects can suffer from fragmentation, as seen in bug 7831. However, this doesn't usually occur until individual allocations hit half of the megablock size, 1M.

Resources