You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1744 lines
72 KiB

=================
Memory management
=================
Overview
========
Duktape memory management is based on the following basic concepts:
* **Allocation functions**.
The user provides a set of functions for allocating, reallocating, and
freeing blocks of memory. These "raw" functions can be used directly, but
the implementation also provides variants which behave the same as the raw
functions externally but force a garbage collection if an allocation
attempt fails due to out of memory. Both of these variants are used
internally, and can also be used by external code.
* **Heap element tracking**.
Actual memory management happens on the heap level. Heap elements
are tracked after being allocated, which allows unreachable elements
to be freed by reference counting and/or mark-and-sweep garbage collection.
Freeing a heap causes all related allocations to be freed, regardless of
their reference count or reachability.
* **Reference counting and/or mark-and-sweep**.
These algorithms are used to detect which heap elements can be
freed. A finalizer method may be executed when an element is
about to be freed by reference counting or mark-and-sweep.
This document covers the memory management related aspects of the
implementation:
* The raw allocation functions and their behavior
* The heap memory layout (for "tracked" allocations)
* Details of the reference counting algorithm
* Details of the mark-and-sweep algorithm
* Implementation notes, such as how to manage reference counting
correctly, how code must be structured to work correctly with
potential ``longjmp()``\ s, etc
Duktape supports three basic models for memory management; one of these
is selected during build:
#. Reference counting and mark-and-sweep, has reclamation for reference loops
#. Reference counting alone, has no reclamation for reference loops
#. Mark-and-sweep alone, has reclamation for reference loops but memory
usage fluctuates considerably between mark-and-sweep collections
At a high level, the implementation code must ensure that reference counts
and heap element reachability are consistently and correctly updated where
reference relationships are changed. In particular, reachability and
reference counts must be correct whenever an operation which may cause a
``longjmp()`` or a garbage collection is performed. This is very tricky in
practice. There is a "GC torture" compilation option to shake out memory
management bugs.
Some terminology
================
Heap element
The term "heap-allocated element" or "heap element" is used to refer broadly
to all memory allocations which are automatically tracked. The term
"heap-allocated object" or "heap object" is not used because it is easy to
confuse with other notions of an "object". In particular, all Ecmascript
objects are heap elements, but there are other heap element types too.
Heap-allocated elements subject to memory management are:
* ``duk_hstring``
* ``duk_hobject`` and its subtypes
* ``duk_hbuffer``
Only ``duk_hobject`` contains further internal references to other heap
elements. These references are kept in the object property table and the
object internal prototype pointer. Currently only ``duk_hobject`` may
have a finalizer.
Heap elements have a **stable pointer** which means that the (main) heap
element is not relocated during its lifetime. Auxiliary allocations
referenced by the heap element (such as an object property table) can be
reallocated/relocated.
Reference
A pointer from a source heap element to a target heap element. The
reference count of the target heap element must be incremented when
a reference is created and decremented when the reference is removed.
Only ``duk_hobject`` heap elements currently contain references,
either through object properties (keys and values) or the object
internal prototype reference.
Borrowed reference
A pointer from a source heap element to a target heap element which
is not reflected in the target element's reference count. Borrowed
references can be used when an actual reference is guaranteed to
exist somewhere while the borrowed reference is in use. If this cannot
be guaranteed, the resulting bugs will be very difficult to diagnose.
Weak reference
A pointer to a target heap element which is not reflected in the target
element's reference count. A weak reference can exist even when no other
references to the target exists, and does not prevent collection of the
target. However, if the target is collected, the weak reference must be
deleted to avoid dangling pointers.
Currently there is no user visible support for weak references as such.
Weak references would be useful for e.g. cache data structures. However,
there are specialized internal weak references which need to be taken into
account. For instance, there is a "string access cache" which optimizes
access to individual characters of strings. This cache weakly references
heap strings and must be updated when strings are collected.
Finalizer
Objects (``duk_hobject`` and its subtypes) stored in the heap may have a
finalizer, which is called when the object is about to be freed. This
allows user code to e.g. free native resources related to the object.
A finalizer could, for instance, close a native socket or free memory
allocated outside Duktape tracking. Finalizers are not required or
supported by the E5 standard. Finalizers require a separate implementation
mechanism for reference counting and mark-and-sweep; these two
implementations need to coexist peacefully.
Allocation functions
====================
Raw functions
-------------
When creating an ``duk_heap``, three memory allocation related functions
are associated with the heap: ``alloc``, ``realloc``, and ``free``.
The related typedefs are::
typedef void *(*duk_alloc_function) (void *udata, size_t size);
typedef void *(*duk_realloc_function) (void *udata, void *ptr, size_t size);
typedef void (*duk_free_function) (void *udata, void *ptr);
The semantics of these functions are essentially the same as their ANSI C
equivalents. In particular:
* The return value for a zero-sized ``alloc`` and ``realloc`` may be
``NULL`` or some non-``NULL``, unique pointer value. Whatever the return
value is, it must be accepted by ``realloc`` and ``free``.
* ``realloc(NULL, size)`` is equivalent to ``malloc(size)``.
* ``realloc(ptr, 0)`` is equivalent to ``free(ptr)`` (assuming ``ptr``
is not ``NULL``), and must either return ``NULL`` or some non-``NULL``
unique pointer value accepted by ``realloc`` and ``free``.
* ``free(NULL)`` is a no-op.
The default implementations map directly to the corresponding ANSI C
functions (``udata`` is ignored). If the platform allocator does not
fulfill the ANSI C requirements, replacement functions must be provided
by user code.
The memory returned by the allocation and reallocation functions must be
properly aligned to support Duktape data structures. In particular, it
must be possible, as far as alignment is concerned, to store a ``double``
or an ``int64_t`` at the start of the returned memory. This does always
imply alignment by 8: on x86 there is usually no alignment requirement at
all, while on ARM alignment by 4 usually suffices. Even when not strictly
required, some level of alignment is often good for performance.
(Technically these alignment requirements differ from the ANSI C
requirements, especially when the allocation size is smaller than 8 bytes,
but these cases don't really matter with Duktape.)
Internal macros
---------------
The following internal macros use the raw allocation functions and do
not trigger garbage collection or any other side effects:
* ``DUK_ALLOC_RAW``
* ``DUK_REALLOC_RAW``
* ``DUK_FREE_RAW``
The natural downside of using these functions is that an allocation or
reallocation may fail even if some memory would be available after a
garbage collection.
The following internal macros may trigger a garbage collection (even
when not strictly out of memory):
* ``DUK_ALLOC``
* ``DUK_ALLOC_ZEROED``
* ``DUK_REALLOC``
* ``DUK_REALLOC_INDIRECT``
* ``DUK_FREE``
Triggering a garbage collection has a wide set of possible side effects.
If a finalizer is executed, arbitrary Ecmascript or even native code
may run. Garbage collection side effects are discussed in detail in a
separate section below.
Memory reallocation (e.g. ``DUK_REALLOC()``) has a particularly nasty
interaction with garbage collection. Mark-and-sweep side effects may
potentially change the original pointer being reallocated. This must
be taken into account when retrying the reallocation operation.
There is a separate macro for these cases, ``DUK_REALLOC_INDIRECT()``,
see detailed discussion below.
Note that even if user code is allocating buffers to be used outside
of automatic memory management, the garbage collection triggering
variants are usually preferable because memory pressure is then communicated
properly between user allocations and Duktape managed allocations. Use the
raw variants only when invoking a garbage collection would be detrimental;
this is rarely the case, especially for user code.
Because a (non-raw) memory allocation or reallocation may invoke garbage
collection, any function or macro call which allocates memory directly or
indirectly may have such side effects. Any direct or indirect checked
memory allocations may also throw an out-of-memory error (leading ultimately
to a ``longjmp()``).
Public API
----------
The heap-associated memory allocation functions can also be called by user
code through the exposed API. This is useful for e.g. C functions which
need temporary buffers. Note, however, that such allocations are, of course,
not automatically managed so care must be taken to avoid memory leaks caused
by e.g. errors (``longjmp()``\ s) in user code and the functions it calls.
The raw API calls behave essentially as direct wrappers for the memory
management functions registered into the heap. The API calls providing
garbage collection are unchecked and simply return a ``NULL`` on errors.
A ``NULL`` is only returned when an allocation request cannot be satisfied
even after garbage collection. Expect in fatal errors, the API calls are
guaranteed to return and will hide e.g. errors thrown by finalizer functions.
Another alternative, perhaps more robust, is to push a ``buffer`` object into
the value stack; the buffer will be automatically memory managed. Also, if
the buffer is a fixed size one, a stable pointer can be obtained after
allocation and passed anywhere in user code without further checks. The
buffer is viable until it is no longer reachable (i.e. is pushed off the value
stack and is not stored in any reachable object or variable).
The public API is::
/* no garbage collection */
void *duk_alloc_raw(duk_context *ctx, size_t size);
void duk_free_raw(duk_context *ctx, void *ptr);
void *duk_realloc_raw(duk_context *ctx, void *ptr, size_t size);
/* may cause garbage collection, doesn't longjmp() */
void *duk_alloc(duk_context *ctx, size_t size);
void duk_free(duk_context *ctx, void *ptr);
void *duk_realloc(duk_context *ctx, void *ptr, size_t size);
DUK_REALLOC() issues with mark-and-sweep; DUK_REALLOC_INDIRECT()
----------------------------------------------------------------
There is a subtle gotcha when using DUK_REALLOC(). If the initial attempt
to reallocate fails, the DUK_REALLOC() implementation will trigger a
mark-and-sweep and then retry the reallocation. This does not work if the
mark-and-sweep may have an effect on the original pointer being reallocated.
In that case, the second attempt to reallocate will use an invalid "original
pointer"!
A more conrete example of reallocating a valuestack (``thr->valstack``):
* Calling code calls ``DUK_REALLOC(thr, thr->valstack, new_size)``.
Assume that the value of ``thr->valstack`` is ``P1`` at this point.
* The ``DUK_REALLOC()`` implementation attempts to use the raw realloc,
giving ``P1`` as its pointer argument. This attempt fails.
* A mark-and-sweep is triggered. The mark-and-sweep invokes a number
of finalizer methods, which cause **the same valstack** to be resized.
This resize succeeds, and ``thr->valstack`` pointer is updated to ``P2``.
* The ``DUK_REALLOC()`` implementation retries the raw realloc, again
giving ``P1`` as its pointer argument. Here, ``P1`` is a garbage
pointer and the realloc call has undefined behavior.
The correct pointer for the second realloc would be ``P2``. However,
the helper behind the macro doesn't know where the pointer came from.
A naive approach is to use an indirect realloc function which gets a
pointer to the storage location of the pointer being reallocated
(e.g. ``(void **) &thr->valstack``). The realloc implementation then
re-lookups the current pointer right before every reallocation, which
works correctly even if the pointer has changed by garbage collection.
Note that heap headers have stable pointers so that the header which
contains the pointer is never relocated so the location of the pointer
itself never changes. Even so, this approach suffers from C type-punning
and strict aliasing issues. Such issues could be fixed by changing all
the base pointers to a union but this would be very invasive, of course.
The current solution is to use an indirect realloc function which gets
a callback function with a userdata pointer as its argument. The
callback is used to request for the current value of the pointer being
reallocated. This bloats code to be strict aliasing compatible, but
is the most portable way.
Implications:
* DUK_REALLOC_RAW() can be used reliably for anything, but is not guaranteed
to succeed (even if memory would be available after garbage collection).
* DUK_REALLOC() can be used reliably for pointers which are guaranteed not to
be affected by mark-and-sweep -- considering that mark-and-sweep runs
arbitrary code, including even arbitrary native function, e.g. as part of
object finalization.
* DUK_REALLOC_INDIRECT() (or DUK_ALLOC() + DUK_FREE()) should be used for
pointers which are not stable across a mark-and-sweep. The storage
location of such pointers must be stable, e.g. reside in the meain
allocation of a heap object.
Heap structure
==============
Overview
--------
All heap-allocated elements must be recorded in the ``duk_heap``, either as
part of the string table (for ``duk_hstring`` elements) or as part of the
"heap allocated" list (or temporary work queues). This is required so that
all allocated elements can always be enumerated and freed, regardless of their
reference counts or reachability.
Heap elements which are currently in use somewhere must have a positive
reference count, and they must be reachable through the actual reachability
roots starting from the ``duk_heap`` structure. These form the actual
reachability graph from a garbage collection point of view; any objects
tracked by the heap but not part of the reachability graph are garbage
and can be freed. Such objects, assuming reference counts are correct,
either have a zero reference count or belong to a reference cycle.
The following figure summarizes the elements managed by a single
heap structure, with arrows indicating basic reachability or
ownership relationships::
All non-string heap elements reside in one of the
following object lists:
* "heap allocated"
* "refzero work list"
* "mark-and-sweep finalization work list"
+-------------+ h_next +-------------+ h_next
.------>| duk_hobject |<-------->| duk_hbuffer |<--------> ...
| +-------------+ (h_prev) +-------------+ (h_prev)
|
+==========+ (Above illustrates "heap allocated", there are
| duk_heap | similar lists for "refzero" and "finalization")
+==========+
| |
| |
| | All duk_hstrings reside in the string table.
| |
| | +--------+
| | : string : +-------------+
| +------>: intern :----->| duk_hstring |
| | : table : +-------------+
| | +--------+ ^ ^
| | | :
| | +------+ | :
| +------>: strs :-----------' :
| | +------+ (built-in :
| | strings) :
| | +--------+ :
| `------>: string : :
| : access :- - - - - - - - '
| : cache : (weak refs)
| +--------+
|
|
| (reachability graph roots)
|
| +-------------+
+---> | duk_hthread | heap_thread: internal thread, also used
| +-------------+ for (some) finalization
|
| +-------------+
`---> | duk_hthread | curr_thread: currently running thread
+-------------+
|
|
| +----------+ +-------------+
+--->: builtins :----->| duk_hobject |
| +----------+ +-------------+
| |
| +--> object properties
| |
| `--> (type specific)
+--> object properties
|
+--> value stack
|
+--> call stack
|
+--> catch stack
| +-------------+
`--> resumer -----------> | duk_hthread |
(another duk_hthread +-------------+
or NULL)
Notation::
+=====+ +-----+ +-----+
| xxx | | xxx | : xxx :
+=====+ +-----+ +-----+
backbone heap element auxiliary
(Many details are omitted from the figure; for instance, there are
back pointers and duplicate pointers for faster access which are not
illustrated at all.)
The primary memory management models relate to the figure as follows
(omitting details such as recursion depth limits, finalization, interaction
between reference counting and mark-and-sweep, etc):
* Reference counting works by inspecting a reference count field which
is a part of the header of every heap allocated element (including
strings). Whenever a reference is removed, the reference count of
the target is decreased, and if the reference count becomes zero, the
target object can be freed. Before freeing, any outgoing references
from object must be iterated and the reference count of the target
heap elements needs to be decreased, possibly setting off a cascade
of further "refzero" situations. Note that incoming references don't
need to be considered: if reference counts are correct and the reference
count of the current object is zero, there cannot be any live incoming
references.
* Mark-and-sweep works by traversing the reachability graph originating
from the ``duk_heap`` structure referenced, marking all reachable objects,
and then walking the comprehensive "heap allocated" list to see which
objects are unreachable and can be freed.
The only "backbone" element which is not itself a heap element is the
``duk_heap`` object. Heap elements include both internal and external
objects which may reference each other in an arbitrary conceptual graph.
Finally, auxiliary elements are either struct members or additional
allocations "owned" by the main heap element types. They are an integral
part of their parent element and cannot be referenced directly by other
elements. They are freed when their parent is freed.
The primary roots for reachability are the threads referenced by the heap
object. In particular, the currently running thread is reachable, and the
thread structure maintains a pointer to the thread which resumed the current
thread (if any). All heap element references ultimately reside in:
* Object properties
* Thread value stack
* Thread call stack
* Thread catch stack
* Thread resumer reference
* Compiled function constant table
* Compiled function inner function table
These references form the heap-level reachability graph, and provides
the basis for mark-and-sweep collection.
There are, of course, temporary references to both heap-allocated and
non-heap-allocated memory areas in CPU registers and the stack frames
of the C call stack. Such references must be very carefully maintained:
an abrupt completion (concretely, a ``longjmp()``) will unwind the C
stack to some catch point (concretely, a ``setjmp()``) and any such
references are lost. Also, any unreachable heap elements may be freed
if a mark-and-sweep is triggered directly or indirectly. See separate
discussion on error handling and memory management.
Heap elements
-------------
All heap tracked elements have a shared header structure, ``duk_heaphdr``,
defined in ``duk_heaphdr.h``. String elements use a smaller
``duk_heaphdr_string`` header which is a prefix of ``duk_heaphdr``.
The difference between these two headers is that ``duk_heaphdr_string``
does not contain next/previous links required to maintain heap allocated
objects in a single or double linked list. These are not needed because
strings are always kept in the heap-level string intern table, and are
thus enumerable (regardless of their reachability) through the string
intern hash table.
Heap-allocated elements are always allocated with a fixed size, and are
never reallocated (and hence never moved) during their life cycle. This
allows all heap-allocated elements to be pointed to with *stable pointers*.
Non-fixed parts of an element are allocated separately and pointed to by
the main heap element. Such allocations are "owned" by the heap element
and are automatically freed when the heap element is freed. The upside of
having stable pointers is simplicity and compatibility with existing
allocators. The downside is that memory fragmentation may become an issue
over time because there is no way to compact the heap. The full size of
the fixed part of the heap element needs to be known at the time of
allocation.
Normally, heap elements are typed by the tagged value (``duk_tval``)
which holds the heap pointer, or if the heap element reference is in
a struct field, the field is usually already correctly typed through its
C type (e.g. a field might have the type "``duk_hcompfunc *``").
However, heap elements do have a "heap type" field as part of the
``h_flags`` field of the header; this is not normally used, but is
needed by e.g. reference counting. As a separate issue, some heap types
(such as ``duk_hobject``) have "sub-types" with various extended memory
layouts; these are not reflected in the heap type.
The current specific heap element types are:
* ``duk_hstring`` (heap type ``DUK_HTYPE_STRING``):
+ Fixed size allocation consisting of a header with string
data following the header. Header does not contain next/previous
pointers (uses ``duk_heaphdr_string``).
+ No references to other heap elements.
* ``duk_hobject`` (heap type ``DUK_HTYPE_OBJECT``):
+ Fixed size allocation consisting of a header, whose size
depends on the object type (``duk_hobject``, ``duk_hthread``,
``duk_hcompfunc``, ``duk_hnatfunc``, etc).
+ The specific "sub type" and its associated struct definition
can be determined using object flags, using the macros:
- ``DUK_HOBJECT_IS_COMPFUNC``
- ``DUK_HOBJECT_IS_NATFUNC``
- ``DUK_HOBJECT_IS_THREAD``
- (and other sub types added later)
- If none of the above are true, the object is a plain object
(``duk_hobject`` without any extended structure)
+ Properties are stored in a separate, dynamic allocation, and contain
references to other heap elements.
+ For ``duk_hcompfunc``, function bytecode, constants, and
references to inner functions are stored in a fixed ``duk_hbuffer``
referenced by the ``duk_hcompfunc`` header. These provide
further references to other heap elements.
+ For ``duk_hthread`` the heap header contains references to the
value stack, call stack, catch stack, etc, which provide references
to other heap elements.
* ``duk_hbuffer`` (heap type ``DUK_HTYPE_BUFFER``):
+ Fixed buffer (``DUK_HBUFFER_HAS_DYNAMIC()`` is false):
- Fixed size allocation consisting of a header with buffer data
following the header.
+ Dynamic buffer (``DUK_HBUFFER_HAS_DYNAMIC()`` is true):
- Fixed size allocation consisting of a header with a pointer to
the current buffer allocation following the header.
- Buffer data is allocated separately and the buffer may be resized.
The address of the buffer data may change during a resize.
+ No references to other heap elements.
String table
============
String interning
----------------
All strings are `interned`__ into the hash level string table: only one,
immutable copy of any particular string is ever stored at a certain
point in time.
.. __: http://en.wikipedia.org/wiki/String_interning
When a new string is constructed e.g. by string concatenation, the
string table is checked to see if the resulting string has already been
interned. If yes, the existing string is used; if not, the string is added
to the string table. Regardless, the string is represented by an
``duk_hstring`` pointer which is stable for the lifetime of the string.
String interning has many nice features:
* When a string is interned, precomputations can be done and stored as
part of the string representation. For example, a string hash can be
precomputed and used elsewhere in e.g. hash tables. Other precomputations
would also be possible, e.g. numeric conversions (not currently used).
* Strings can be compared using direct pointer comparisons without comparing
actual string data, since at any given time, a given string can only have
one ``duk_hstring`` instance with a stable address.
* Memory is saved for strings which occur multiple times. For instance,
object properties of the same name are simply referenced with a string
pointer instead of storing multiple instances of the same property name.
But, there are downsides as well:
* String manipulation is slower because any intermediate, referenceable
results need to be interned (which implies string hashing, a lookup
from the string table, etc). This can be mitigated e.g. by doing string
concatenation of multiple parts in an atomic fashion.
* For small strings which only occur once or twice in the heap, there is
additional overhead in the interned ``duk_hstring`` heap element compared
to simply storing the string in an object's property table, for instance.
* Using string values as "data buffers" which are continuously manipulated
(appended or predended to, sliced, etc) is very inefficient and causes a
lot of garbage collection churn. Buffer objects should be used instead,
but these are not part of the Ecmascript standard.
Memory management of strings
----------------------------
Interned strings are garbage collected normally when they are no longer needed.
They are later re-interned if they are needed again; at this point they usually
get a different pointer than before.
String table algorithm
----------------------
The string table structure is similar to the "entry part" of the
``duk_hobject`` property allocation:
* Closed hash table (probe sequences). Probe sequences use an initial
index based on string hash value, and a probe step looked up from a
precomputed table of step values using a string hash value based index.
* Hash table size is rounded upwards to a prime in a precomputed
sequence. Hash table load factor is kept within a certain range
by resizing whenever necessary.
* Deleted entries are explicitly marked DELETED to avoid breaking
hash probe chains. DELETED entries are eliminated on rehashing,
and are counted as "used" entries before a resize to ensure there
are always NULL entries in the string table to break probe sequences.
For more details, see:
* ``hstring-design.rst`` for discussion on the string hash algorithm.
* ``hobject-design.rst``, entry part hash algorithm, for discussion on
the basic closed hash structure.
.. note:: This discussion should be expanded.
Reference counting
==================
Introduction
------------
For background, see:
* http://en.wikipedia.org/wiki/Reference_counting
In basic reference counting each heap object has a reference count field
which indicates how many other objects in the heap point to this object.
Whenever a new reference is created, its target object's reference count
is incremented; whenever a reference is destroyed, its target object's
reference counter is decreased. If a reference count goes to zero when it
is decreased, the object can be freed directly. When the object is freed,
any heap objects it refers to need to have their reference counts decremented,
which may trigger an arbitrarily long chain of objects to be freed recursively.
There are variations of reference counting where objects are not freed immediately
after their reference count goes to zero. Objects-to-be-freed can be managed in
a work list and freed later. However, for our purposes it is useful to free any
reference counted objects as soon as possible (otherwise we could just use the
mark-and-sweep collector).
There are also reference counting variants which handle reference loops
correctly without resorting to mark-and-sweep. These seem to be too complex
in practice for a small interpreter.
Reference counting increases code size, decreases performance due to
reference count updates, and increases heap header size for every object.
On the other hand it minimizes variance in memory usage (compared to plain
mark-and-sweep, even an incremental one) and is very useful for small
scripts running without a pre-allocated heap. Reference counting also reduces
the impact of having non-relocatable heap elements: memory fragmentation still
happens, but is comparable to memory fragmentation encountered by ordinary
C code.
Reference count field
---------------------
The reference count field is embedded into the ``duk_heaphdr`` structure
whose layout varies depending on the memory management model chosen for
the build. The reference count field applies to all heap allocated elements,
including strings, so it appears in the header before the next/previous
pointers required for managing non-string heap elements.
The current struct definitions are in ``duk_heaphdr.h``. Two structures
are defined:
* ``duk_heaphdr``: applies to all heap elements except strings.
* ``duk_heaphdr_string``: applies to strings, beginning of struct matches
``duk_heaphdr``.
The reference count field must have enough bits to ensure that it will never
overflow. This is easy to satisfy by making the field as large as a data
pointer type. Currently ``size_t`` is used which is technically incorrect
(one could for instance have a platform with maximum allocation size of
32 bits but a memory space of 64 bits).
Reference count macros
----------------------
Macros:
* ``DUK_TVAL_INCREF``
* ``DUK_TVAL_DECREF``
* ``DUK_HEAPHDR_INCREF``
* ``DUK_HEAPHDR_DECREF``
* and a bunch of heap element type specific INCREF/DECREF macros and
helpers, defined in ``heaphdr.h``
Notes on macro semantics:
* The macros are optimized for performance and don't tolerate a ``NULL``
pointer by default. There are ``_ALLOWNULL`` variants for cases where
NULLs may actually occur.
* An ``INCREF`` is guaranteed not to have any side effects.
* A ``DECREF`` may have a wide variety of side effects.
+ ``DECREF`` may free the target object and an arbitrary number of other
objects whose reference count drops to zero as a result.
+ If a finalizer is invoked, arbitrary C or Ecmascript code is
executed which may have essentially arbitrary side effects,
including triggering the mark-and-sweep garbage collector.
+ The mark-and-sweep garbage collector may also be voluntarily
invoked at the end of "refzero" handling.
+ Any ``duk_tval`` pointers pointing to dynamic structures (like
a value stack) may be invalidated; heap element pointers are not
affected because they are stable.
See discussion on "side effects" below for more particulars on the
implementation impact.
Updating reference counts
-------------------------
Updating reference counts is a bit tricky. Some important rules:
* Whenever a ``longjmp()`` or garbage collection may occur, reachability
and reference counts must be correct.
* If a reference count drops to zero, even temporarily, the target is
*immediately* freed. If this is not desired, ``INCREF``/``DECREF``
order may need to be changed.
* A ``DECREF`` call may invalidate *any* ``duk_tval`` pointers to
resizable locations, such as the value stack. It may also invalidate
indices to object property structures if a property allocation is
resized. So, ``DECREF`` must be called with utmost care.
Note that it is *not enough* to artificially increase a target's reference
count to prevent the object from being freed, at least when mark-and-sweep
collection is also enabled. Mark-and-sweep may be triggered very easily,
and *will* free an unreachable object, regardless of its reference count,
unless specific measures are taken to avoid it. In fact, mark-and-sweep
*must* collect unreachable objects with a non-zero reference count, to deal
with reference loops which cannot be collected using reference counting
alone. Even if mark-and-sweep issues were avoided (perhaps with a flag
preventing collection), if a reference count is artificially increased
without there being a corresponding, actual heap-based reference to the
target, there must be a guarantee that the reference count is also decreased
later. This would require a ``setjmp()`` catchpoint.
Specific considerations:
* ``DECREF`` + ``INCREF`` on the same target object is dangerous. If the
refcount drops to zero between the calls, the object is freed. It's
usually preferable to do ``INCREF`` + ``DECREF`` instead to avoid this
potential issue.
The INCREF algorithm
--------------------
The ``INCREF`` algorithm is very simple:
1. If the target reference is ``NULL`` or the target is not a heap element,
return.
2. Increase the target's reference count by one.
The practical implementation depends on whether ``INCREF`` is used on a
tagged value pointer or a heap element pointer.
The DECREF algorithm
--------------------
The ``DECREF`` algorithm is a bit more complicated:
1. If the target reference is ``NULL`` or the target is not a heap element,
return.
2. Decrease the target's reference count by one.
3. If the reference count dropped to zero:
a. If mark-and-sweep is currently running, ignore and return.
(Note: mark-and-sweep is expected to perform a full reachability
analysis and have correct reference counts at the end of the
mark-and-sweep algorithm.)
b. If the target is a string:
1. Remove the string from the string table.
2. Remove any references to the string from the "string access cache"
(which accelerates character index to byte index conversions).
Note that this is a special, internal "weak" reference.
3. Free the string. There are no auxiliary allocations to free
for strings.
4. Return.
c. If the target is a buffer:
1. Remove the buffer from the "heap allocated" list.
2. If the buffer is dynamic, free the auxiliary buffer (which is
allocated separately).
3. Free the buffer.
4. Return.
d. Else the target is an object:
1. Move the object from the "heap allocated" list to the "refzero" work
list. Note that this prevents the mark-and-sweep algorithm from
freeing the object (the "sweep" phase does not affect objects in the
"refzero" work list).
2. If the "refzero" algorithm is already running, return.
3. Else, call the "refzero" algorithm to free pending objects.
The refzero algorithm returns when the entire work list has
been successfully cleared.
4. Return.
The REFZERO algorithm
---------------------
The ``DECREF`` algorithm ensures that only one instance of the "refzero"
algorithm may run at any given time. The "refzero" work list model is used
to avoid an unbounded C call stack depth caused by a cascade of reference
counts which drop to zero.
The algorithm is as follows:
1. While the "refzero" work list is not empty:
a. Let ``O`` be the element at the head of the work list.
Note:
* ``O`` is always an object, because only objects are placed in the work list.
* ``O`` must not be removed from the work list yet. ``O`` must be on the
work list in case a finalizer is executed, so that a mark-and-sweep
triggered by the finalizer works correctly (concretely: to be able to
clear the ``DUK_HEAPHDR_FLAG_REACHABLE`` of the object.)
b. If ``O`` is an object (this is always the case, currently), and has a
finalizer (i.e. has a ``_Finalizer`` internal property):
1. Create a ``setjmp()`` catchpoint.
2. Increase the reference count of ``O`` temporarily by one (back to 1).
3. Note: the presence of ``O`` in the "refzero" work list is enough to
guarantee that the mark-and-sweep algorithm will not free the object
despite it not being reachable.
4. Call the finalizer method. Ignore the return value and a possible
error thrown by the finalizer (except for debug logging an error).
Any error or other ``longjmp()`` is caught by the ``setjmp()``
catchpoint. Note:
* The thread used for finalization is currently the thread which
executed ``DECREF``. *This is liable to be changed later.*
5. Regardless of how the finalizer finishes, decrease the reference
count of ``O`` by one.
6. If the reference count of ``O`` is non-zero, the object has been
"rescued" and:
a. Place the object back into the "heap allocated" list (and debug
log the object as "rescued").
b. Continue the while-loop with the next object.
c. Remove ``O`` from the work list.
d. Call ``DECREF`` for any references that ``O`` contains (this is
called "refcount finalization" in the source). Concretely:
* String: no internal references.
* Buffer: no internal references.
* Object: properties contain references; specific sub-types (like
``duk_hthread``) contain further references.
* Note: this step is recursive with respect to ``DECREF`` but not
the "refzero" algorithm: a ``DECREF`` is executed inside a
``DECREF`` which started the "refzero" algorithm, but the inner
``DECREF`` doesn't restart the "refzero" algorithm. Recursion is
thus limited to two levels.
e. Free any auxiliary references (such as object properties) contained
in ``O``, and finally ``O`` itself.
2. Check for a voluntary mark-and-sweep.
Notes:
* "Churning" the work list requires that the type of a heap element can be
determined by looking at the heap header.
+ This is one of the rare places where this would be necessary: usually the
tagged type of a ``duk_tval`` is sufficient to type an arbitrary value,
and when following pointer references from one heap element to another,
the pointers themselves are typed.
+ Right now, this type determination is not actually needed because only
object (``duk_hobject``) values will be placed in the work list.
* The finalizer thread selection is not a trivial issue, especially for
mark-and-sweep. See discussion under mark-and-sweep.
* Because the reference count is artifially increased by one during finalization,
the object being finalized cannot encounter a "refcount drops to zero"
situation while being finalized (assuming of course that all ``INCREF`` and
``DECREF`` calls are properly "nested").
* If mark-and-sweep is triggered during finalization, the target may or
may not be reachable, but will have a non-zero reference count in
either case due to the artificial ``INCREF`` in the finalization
algorithm. The reference count is inconsistent with the actual reference
count in the reachability graph but this is not an issue for mark-and-sweep.
In any case, mark-and-sweep will not free any object in the "refzero" work
list, regardless of its reachability status, so mark-and-sweep during
REFZERO is not a problem.
* Although finalization increases C call stack size, another finalization
triggered by reference counting cannot occur while finalization for one
object is in progress: any objects whose refcounts drop to zero during
finalization are simply placed in the refzero work list and dealt with
when the object being finalization has been fully processed. However,
there can still be **two** active finalizers at the same time, one initiated
by reference counting and another by a mark-and-sweep triggered inside
REFZERO.
Background on the refzero algorithm, limiting C recursion depth
---------------------------------------------------------------
When a reference count drops to zero, the heap element will be freed. If the
heap element contains references (like an Ecmascript object does), all target
elements need to be ``DECREF``'d before the element is freed. These ``DECREF``
calls may cause the reference count of further elements to drop to zero; this
"cascade" of zero reference counts may be arbitrarily long. Since we need to
live with limited and sometimes very small C stacks in some embedded
environments (some environments may have less than 64 kilobytes of usable
stack), the reference count zero handling must have a limited C recursion
level to work reliably.
This is currently handled by using a "work list" model. Heap elements whose
reference count has dropped to zero are placed in a "to be freed" work list
(see ``duk_heap`` structure, ``refzero_list`` member in ``duk_heap.h``). The
list is then freed using a loop which frees one element at a time until the
list is free. New elements may be added to the list while it is being iterated.
The C recursion level is fixed.
The ``h_prev``/``h_next`` fields of the ``duk_heaphdr`` structure, normally
used for the "heap allocated" list, are used for the "refzero" work list.
Because ``duk_hstring``\ s do not have embedded references so they are freed
directly when their reference count drops to zero. This is fortunate, because
strings don't have ``h_prev``/``h_next`` fields at all.
*Finalization* of an object whose refcount becomes zero is very useful for
e.g. freeing any native resources or handles associated with an object.
For instance, socket or file handles can be closed when the object is being
freed. The finalizer is an internal method associated with an ``duk_hobject``
which is called just before the object is freed either by reference counting
or by the mark-and-sweep collector. The finalizer gets a reference to the
object in question, and may "rescue" the reference.
Mark-and-sweep may be triggered during the "refzero" algorithm, currently
only by finalization. If mark-and-sweep is triggered, it must not touch any
object in the "refzero" work list (i.e. any object whose reference count is
zero, but which has not yet been processed).
Mark-and-sweep
==============
Introduction
------------
For background, see:
* http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)
The variant used is a "stop the world" mark-and-sweep collector, which
is used instead of an incremental one for simplicity and small footprint.
When combined with reference counting, the mark-and-sweep collector is
only required for handling reference cycles anyway, so the particular
variant is not that important. A definite downside of a "stop the world"
collector is that it introduces an annoying pause in application behavior
which is otherwise avoided by reference counting.
The mark-and-sweep algorithm used has support for:
* object finalization (requires two collector passes)
* object compaction (in emergency mode)
* string table resizing
An "emergency mode" is provided for situations where allocation fails
repeatedly, even after a few ordinary mark-and-sweep attempts. In
emergency mode the collector tries to find memory even by expensive
means (such as forcibly compacting object property allocations).
Control flags are also provided to limit side effects of mark-and-sweep,
which is required to implement a few critical algorithms: resizing the
string table, and resizing object property allocation. During these
operations mark-and-sweep must avoid interfering with the object being
resized.
Mark-and-sweep flags
--------------------
Mark-and-sweep control flags are defined in ``duk_heap.h``:
* ``DUK_MS_FLAG_EMERGENCY``
* ``DUK_MS_FLAG_NO_STRINGTABLE_RESIZE``
* ``DUK_MS_FLAG_NO_FINALIZERS``
* ``DUK_MS_FLAG_NO_OBJECT_COMPACTION``
In addition to the explicitly requested flags, the bit mask in
``mark_and_sweep_base_flags`` in ``duk_heap`` is bitwise ORed into the
requested flags to form effective flags. The flags added to the "base
flags" control restrictions on mark-and-sweep side effects, and are used
for certain critical sections.
To protect against such side effects, the critical algorithms:
* Store the original value of ``heap->mark_and_sweep_base_flags``
* Set the suitable restriction flags into ``heap->mark_and_sweep_base_flags``
* Attempt the allocation / reallocation operation, *without throwing errors*
* Restore the ``heap->mark_and_sweep_base_flags`` to its previous value
* Examine the allocation result and act accordingly
It is important not to throw an error without restoring the base flags field.
The concrete flags used are:
* String table resize:
+ ``DUK_MS_FLAG_NO_STRINGTABLE_RESIZE``: prevents another stringtable
resize attempt when one is already running
+ ``DUK_MS_FLAG_NO_FINALIZERS``: prevent finalizers from adding new
interned strings to the string table, possibly requiring a resize
+ ``DUK_MS_FLAG_NO_OBJECT_COMPACTION``: prevent object compaction,
because object compaction may lead to an array part being abandoned,
which leads to string interning of array keys.
* Object property allocation resize:
+ ``DUK_MS_FLAG_NO_FINALIZERS``: prevent finalizers from manipulating
the properties of any object. It would suffice to protect only the
object being resized, but a finalizer may potentially operate on any
set of objects; hence no finalizers are executed at all.
+ ``DUK_MS_FLAG_NO_OBJECT_COMPACTION``: prevent objects from being
compacted (i.e., resized). It would suffice to protect only the
object being resized from a recursive resize; this is currently not
done, however, but would be easy to fix.
Heap header flags
-----------------
The following flags in the heap element header are used for controlling
mark-and-sweep:
* ``DUK_HEAPHDR_FLAG_REACHABLE``:
element is reachable through the reachability graph.
* ``DUK_HEAPHDR_FLAG_TEMPROOT``:
element's reachability has been marked, but its children have not been
processed; this is required to limit the C recursion level.
* ``DUK_HEAPHDR_FLAG_FINALIZABLE``:
element is not reachable after the first marking pass (see algorithm),
has a finalizer, and the finalizer has not been called in the previous
mark-and-sweep round; object will be moved to the finalization work
list and will be considered (temporarily) a reachability root.
* ``DUK_HEAPHDR_FLAG_FINALIZED``:
element's finalizer has been executed, and if still unreachable, object
can be collected. The finalizer will not be called again until this
flag is cleared; this prevents accidental re-entry of the finalizer
until the object is explicitly rescued and this flag cleared.
These are referred to as ``REACHABLE``, ``TEMPROOT``, ``FINALIZABLE``,
and ``FINALIZED`` below for better readability. All the flags are clear
when a heap element is first allocated. Explicit "clearing passes" are
avoided by careful handling of the flags so that the flags are always in
a known state when mark-and-sweep begins and ends.
Basic algorithm
---------------
The mark-and-sweep algorithm is triggered by a failed memory allocation
either in "normal" mode or "emergency" mode. Emergency mode is used if
a normal mark-and-sweep pass did not resolve the allocation failure; the
emergency mode is a more aggressive attempt to free memory. Mark-and-sweep
is controlled by a set of flags. The effective flags set is a bitwise OR
of explicit flags and "base flags" stored in ``heap->mark_and_sweep_base_flags``.
The "base flags" essentially prohibit specific garbage collection operations
(like finalizers) when a certain critical code section is active.
The mark-and-sweep algorithm is as follows:
1. The ``REACHABLE`` and ``TEMPROOT`` flags of all heap elements are
assumed to be cleared at this point.
* Note: this is the case for all elements regardless of whether they
reside in the string table, the "heap allocated" list, the "refzero"
work list, or anywhere else.
2. **Mark phase**.
The reachability graph is traversed recursively, and the ``REACHABLE``
flags is set for all reachable elements. This is complicated by the
necessity to impose a limit on maximum C recursion depth:
a. At the beginning the heap level flag
``DUK_HEAP_FLAG_MARKANDSWEEP_RECLIMIT_REACHED`` is asserted to be
cleared.
b. The reachability graph of the heap is traversed with a depth-first
algorithm:
1. Marking starts from the reachability roots:
* the heap structure itself (including the current thread, its
resuming thread, etc)
* the "refzero_list" for reference counting
2. If the reachability traversal hits the C recursion limit
(``mark_and_sweep_recursion_limit`` member of the heap) for
some heap element ``E``:
a. The ``DUK_HEAP_HAS_MARKANDSWEEP_RECLIMIT_REACHED`` flag is set.
b. The reachability status of ``E`` is updated, but its internal
references are not processed (to avoid further recursion).
c. The ``TEMPROOT`` flag is set for ``E``, indicating that it
should be processed later.
3. Unreachable objects which need finalization (but whose finalizers
haven't been executed in the last round) are marked FINALIZABLE
and are marked as reachable with the normal recursive marking
algorithm.
4. The algorithm of step 2 (handling ``TEMPROOT`` markings) is
repeated to ensure reachability graph has been fully processed
(elements are marked reachable and TEMPROOT flags are set),
also for the objects just marked FINALIZABLE.
c. While the ``DUK_HEAP_HAS_MARKANDSWEEP_RECLIMIT_REACHED`` flag is
set for the heap:
1. Clear the ``DUK_HEAP_HAS_MARKANDSWEEP_RECLIMIT_REACHED`` flag
of the heap.
2. Scan all elements in the "heap allocated" or "refzero work list"
(note that "refzero work list" *must* be included here but not
in the sweep phase). For each element with the ``TEMPROOT`` flag set:
a. Clear the ``TEMPROOT`` flag.
b. Process the internal references of the element recursively,
imposing a similar recursion limit as before (i.e. setting
the ``DUK_HEAP_HAS_MARKANDSWEEP_RECLIMIT_REACHED`` flag etc).
3. **Sweep phase 1 (refcount adjustments)**.
Inspect all heap elements in the "heap allocated" list (string table
doesn't need to be considered as strings have no internal references):
a. If the heap element would be freed in sweep phase 2 (i.e., element
is not reachable, and has no finalizer which needs to be run):
1. Decrease reference counts of heap elements the element points to,
but don't execute "refzero" queueing or the "refzero" algorithm.
Any elements whose refcount drops to zero will be dealt with by
mark-and-sweep and objects in the refzero list are handled by
reference counting.
4. **Sweep phase 2 (actual freeing)**.
Inspect all heap elements in the "heap allocated" list and the string
table (note that objects in the "refzero" work list are NOT processed
and thus never freed here):
a. If the heap element is ``REACHABLE``:
1. If ``FINALIZED`` is set, the object has been rescued by the finalizer.
This requires no action as such, but can be debug logged.
2. Clear ``REACHABLE`` and ``FINALIZED`` flags.
3. Continue with next heap element.
b. Else the heap element is not reachable, and:
1. If the heap element is an ``duk_hobject`` (its heap type is
``DUK_HTYPE_OBJECT``) and the object has a finalizer (i.e. it
has the internal property ``_Finalizer``), and the ``FINALIZED``
flag is not set:
a. Move the heap element from "heap allocated" to "to be finalized"
work list.
b. Continue with next heap element.
2. Free the element and any of its "auxiliary allocations".
3. Continue with next heap element.
5. For every heap element in the "refzero" work list:
a. Clear the element's ``REACHABLE`` flag.
(See notes below why this seemingly unnecessary step is in fact necessary.)
6. If doing an emergency mark-and-sweep and object compaction is not
explicitly prohibited by heap flags:
a. Compact the object's property allocation in the hopes of freeing
memory for the emergency.
7. If string table resize is not explicitly prohibited by heap flags:
a. Compact and rehash the string table. This can be controlled by build
flags as it may not be appropriate in all environments.
8. Run finalizers:
a. While the "to be finalized" work queue is not empty:
1. Select object from head of the list.
2. Set up a ``setjmp()`` catchpoint.
3. Execute finalizer. Note:
* The thread used for this is the currently running thread
(``heap->curr_thread``), or if no thread is running,
``heap->heap_thread``. This is liable to change in the future.
4. Ignore finalizer result (except for logging errors).
5. Mark the object ``FINALIZED``.
6. Move the object back to the "heap allocated" list. The object will
be collected on the next pass if it is still unreachable. (Regardless
of actual reachability, the ``REACHABLE`` flag of the object is clear
at this point.)
9. Finish.
a. All ``TEMPROOT`` and ``REACHABLE`` flags are clear at this point.
b. All "heap allocated" elements either (a) are reachable and have a
non-zero reference count, or (b) were finalized and their reachability
status is unknown.
c. The "to be finalized" list is empty.
d. No object in the "refzero" work list has been freed.
Notes:
* Elements on the refzero list are considered reachability roots, as we need
to preserve both the object itself (which happens automatically because we
don't sweep the refzero_list) and its children. If the refzero list elements
were not considered reachability roots, their children might be swept by the
sweep phase. This would be problematic for processing the objects in the
refzero list, regardless of whether they have a finalizer or not, as some
references would be dangling pointers.
* Elements marked FINALIZABLE are considered reachability roots to ensure
that their children (e.g. property values) are not swept during the
sweep phase. This would obviously be problematic for running the finalizer,
regardless of whether the object would be rescued or not.
* While mark-and-sweep is running:
+ Another mark-and-sweep cannot execute.
+ A ``DECREF`` resulting in a zero reference count is not processed at all.
The object is not placed into the "refzero" work list, as mark-and-sweep
is assumed to be a comprehensive pass, including running finalizers.
* Finalizers are executed after the sweep phase to ensure that finalizers
have as much available memory as possible. Since mark-and-sweep is
running, if a finalizer runs out of memory, no memory can be reclaimed
as recursive mark-and-sweep is explicitly blocked. This is probably a
very minor issue in practice.
* Finalizers could be executed from their work list after the mark-and-sweep
has finished to allow mark-and-sweep to run if mark-and-sweep is required
by a finalizer. The mark-and-sweep could then append more objects to be
finalized into the "to be finalized" work list; this is not a problem.
However, since finalizers are used with a rather limited scope, this is not
currently done.
* The sweep phase is divided into two separate scans: one to adjust refcounts
and one to actually free the objects. If these were performed in a single
heap scan, refcount adjustments might refer to already freed heap elements
(dangling pointers). This may happen even without reference counting bugs
for unreachable reference loops.
* Clearing the ``REACHABLE`` flags explicitly for objects in the "refzero"
list is necessary:
+ The "refzero" work list is not processed at all in the sweep phase but the
marking phase could theoretically mark objects in the "refzero" work list.
Since the sweeping phase is the only place where ``REACHABLE`` flags are
cleared, some object in the "refzero" work list might be left with its
``REACHABLE`` flag set at the end of the algorithm. At first it might seem
that this can never happen if reference counts are correct: all objects in
the "refzero" work list are unreachable by definition. However, this is not
the case for objects with finalizers.
+ A finalizer call made by the "refzero" algorithm makes the object reachable
again (through the finalizer thread value stack; the finalizer method itself
can also create reachable references for the target). If a mark-and-sweep
is triggered during finalization, the target will be marked ``REACHABLE``
during the mark phase. Thus, ``REACHABLE`` flags of "refzero" work list
elements must be cleared explicitly after or during the sweep phase.
Note that there is a small "hole" in the reclamation right now, when
mark-and-sweep finalizers are used:
* If a finalizer executed by mark-and-sweep removes a reference to another
object (not the object being finalized), causing the target object's
reference count to drop to zero, the object is *not* placed in the
"refzero" work list, as mark-and-sweep is still running.
* As a result, the object will be unreachable and will not be freed by
the reference count algorithm, regardless of whether the object was part
of a reference loop. Instead, the next mark-and-sweep will free the object.
If the object has a finalizer, the finalizer will be called later than
would be preferable.
* This is not ideal but will not result in memory leaks, so it's not really
worth fixing right now.
Interactions between reference counting and mark-and-sweep
==========================================================
If mark-and-sweep is triggered e.g. by an out-of-memory condition, reference
counting is essentially "disabled" for the duration of the mark-and-sweep
phase:
* Reference counts are updated normally. In fact, mark-and-sweep uses the
same refcount macros to update element refcounts while freeing them.
* If a reference count reaches zero due after a ``DECREF`` operation, the
object is not freed nor is it placed on the "refzero" work list because
mark-and-sweep is expected to deal with the object directly.
If the "refzero" algorithm is triggered first (with some objects in the
"refzero" work list), mark-and-sweep may be triggered while the "refzero"
algorithm is running. In more detail:
* A ``DECREF`` happens while neither mark-and-sweep nor "refzero" algorithm
is running.
* A reference count reaches zero, and the object is placed on the "refzero"
work list and the "refzero" algorithm is invoked.
* The "refzero" algorithm cannot trigger another "refzero" algorithm to
execute recursively. Instead, the work list is churned until it becomes
empty. Any objects whose reference count reaches zero are added to the
work list, though, so will be processed eventually.
* The "refzero" algorithm may trigger a mark-and-sweep while it is running,
e.g. by running a finalizer which runs out of memory:
+ This mark-and-sweep will mark any elements in the "refzero" work list
but will not free them.
+ While the mark-and-sweep is running, no new elements are placed into
the "refzero" work list, even if their reference count reaches zero.
Instead, the mark-and-sweep algorithm is assumed to deal with them.
+ The mark-and-sweep algorithm may also execute finalizers, so two
finalizers (but no more) can be running simultaneously, though on
different objects.
+ Another recursive mark-and-sweep run cannot happen.
Finalizer behavior
==================
General notes:
* If reference counting is used, finalizers are called either when reference
count drops to zero, or when mark-and-sweep wants to collect the object
(which is required for circular references and may also happen if reference
counts have been incorrectly updated for whatever reason).
* If mark-and-sweep is used, finalizers are called only when mark-and-sweep
wants to collect the object.
* Regardless of whether reference counting or mark-and-sweep (or both) is
used, finalizers are executed for all objects (even reachable ones) when
a heap is freed.
* Finalizer may reinstate a reference to the target object. In this case the
object is "rescued" and its finalizer may be called again if it becomes
unreachable again. Regardless of whether an object is rescued or not,
it's a good practice to make the finalizer re-entrant, i.e. allow multiple
finalizer calls even if the finalizer doesn't rescue the object.
* Finalizers are guaranteed to run when objects are collected or when a heap
is destroyed forcibly. The Duktape API ``duk_destroy_heap()`` call runs a
few rounds of mark-and-sweep to allow finalizers for unreachable objects to
run normally, and then runs finalizers for all objects on the heap_allocated
list regardless of their reachability status. This allows user code to e.g.
free any native resources more or less reliably even for reachable objects.
* The finalizer return value is ignored. Also, if the finalizer throws an
error, this is only debug logged but is considered to be a successful
finalization.
* The thread running a finalizer is not very logical right now and is liable
to change:
+ Reference counting: the thread which executed ``DECREF`` is used as the
finalizer thread.
+ Mark-and-sweep: the thread which caused mark-and-sweep is used as the
finalizer thread; if there is no active thread, ``heap->heap_thread``
is used instead.
* The finalizer may technically launch other threads and do arbitrary things
in general, but it is a good practice to make the finalizer very simple and
unintrusive. Ideally it should only operate on the target object and its
properties.
* A finalizer should not be able to terminate any threads in the active call
stack, in particular the thread which triggered a finalization or the
finalizer thread (if these are different).
Finalizer thread selection is currently not optimal; there are several
approaches:
* The thread triggering mark-and-sweep is not a good thread for finalization,
as it may be from a different conceptual virtual machine, and may thus have
a different global context (global object) than where the finalized object
was created.
* A heap-level dedicated finalizer thread has a similar problem: the finalizer
will run in a different global context than where the finalized object was
created.
Voluntary mark-and-sweep interval
=================================
There are many ways to decide when to do a voluntary mark-and-sweep pass:
byte count based, object count based, probabilistic, etc.
The current approach is to count the number of heap objects and heap strings
kept at the end of a mark-and-sweep pass, and initialize the voluntary sweep
trigger count based on that as (the computation actually happens using fixed
point arithmetic)::
trigger_count = ((kept_objects + kept_strings) * MULT) + ADD
// MULT and ADD are tuning parameters
The trigger count is decreased on every memory (re)allocation, and for every
object processed by the refzero algorithm. If the trigger reaches zero when
memory is about to be (re)allocated, a voluntary mark-and-sweep pass is done.
When ``MULT`` is 1 and ``ADD`` is 0, a voluntary sweep is done when the number
of "operations" matches the previous heap object/string count.
When reference counting is enabled, ``MULT`` can be quite large (e.g. 10)
because only circular references need to be swept. When reference counting
is not enabled, ``MULT`` should be closer to 1 (or even below). The ``ADD``
tuning parameter is not that important; its purpose is to avoid too frequent
mark-and-sweep on very small heaps and to counteract some inaccuracy of fixed
point arithmetic.
Implementation issues
=====================
Error handling
--------------
When a ``longjmp()`` takes place, the C stack is unwound and all references
to the unwound part of the stack are lost. To avoid memory leaks and other
correctness issues, care must be taken to:
* Ensure that the reference count of every heap-allocated element is
correct whenever entering code which may ``longjmp()``.
* Ensure that all heap-allocated objects which should be subject to
automatic garbage collection are reachable whenever entering code
which may ``longjmp()``.
* Use a ``setjmp()`` catchpoint whenever control must be regained to
clean up properly.
To avoid the need for ``setjmp()`` catchpoints, many innermost helper
functions return error codes rather than throwing errors. This makes
error handling a bit easier.
Side effects of memory management
---------------------------------
Automatic memory management may be triggered by various operations, and has
a wide variety of side effects which must be taken into account by calling
code. This affects internal code in particular, which must be very careful
not to reference dangling pointers, deal with valstack and object property
allocation resizes, etc.
The fundamental triggers for memory management side effects are:
* An attempt to ``alloc`` or ``realloc`` memory may trigger a garbage
collection. A collection is triggered by an out-of-memory condition,
but a voluntary garbage collection also occurs periodically. A ``free``
operation cannot, at the moment, trigger a collection.
* An explicit request for garbage collection.
* A ``DECREF`` operation which drops the target heap element reference
count to zero triggers the element (and possibly a bunch of other
elements) to be freed, and may invoke a number of finalizers. Also,
a mark-and-sweep may be triggered (e.g. by finalizers or voluntarily).
The following primitives do not trigger any side effects:
* An ``INCREF`` operation never causes a side effect.
* A ``free`` operation never causes a side effect.
Because of finalizers, the side effects of a ``DECREF`` and a mark-and-sweep
are potentially the same as running arbitrary C or Ecmascript code,
including:
* Calling (further) finalizer functions (= running arbitrary Ecmascript and C code).
* Resizing object allocations, value stacks, catch stacks, call stacks, buffers,
object property allocations, etc.
* Compacting object property allocations, abandoning array parts.
* In particular:
+ Any ``duk_tval`` pointers referring any value stack may be invalidated,
because any value stack may be resized. Value stack indices are OK.
+ Any ``duk_tval`` pointers referring any object property values may be
invalidated, because any property allocation may be resized. Also,
any indices to object property slots may be invalidated due to
"compaction" which happens during a property allocation resize.
+ Heap element pointers are stable, so they are never affected.
The side effects can be avoided by many techniques:
* Refer to value stack using a numeric index.
* Make a copy of an ``duk_tval`` to a C local to ensure the value can still
be used after a side effect occurs. If the value is primitive, it will
OK in any case. If the value is a heap reference, the reference uses a
stable pointer which is OK as long as the target is still reachable.
* Re-lookup object property slots after a potential side effect.
Misc notes
==========
Garbage collection of value stacks
----------------------------------
While an Ecmascript function is running, the value stack frame allocated
for it has a minimum size matching the "register count" of the function.
All of these registers are reachable from a mark-and-sweep viewpoint, even
if the values held by the registers are never referenced by the bytecode
of the function.
For instance, any temporaries created during expression evaluation may
leave unused but technically reachable values behind. Consider for
instance::
function f(x,y,z) {
var w = (x + y) + z;
}
the bytecode created for this will:
* Compute ``x + y`` into a temporary register ``T``.
* Compute ``T + z`` into the register allocated for ``w``.
Before exiting the function, ``T`` is reachable for mark-and-sweep. If
``T`` is a heap element (e.g. a string), it has a positive reference count.
The situation is fixed if the function exits or the temporary register ``T``
is reused by the evaluation of another expression, so this is not usually a
relevant issue. However:
* If a function runs in an infinite loop, such references may never become
collectable. Consider, for instance, a main event loop which never exits.
* Even if the function eventually exits, such references may cause an
out-of-memory situation before the function exits. The out-of-memory
situation may not be recoverable using garbage collection because the
values are technically reachable until the exit.
There is currently no actual solution to this issue, but any code containing
an infinite loop should be structured to avoid "dangling values", e.g. by
using an auxiliary function for any computations::
function stuff() {
// ...
}
function infloop() {
for (;;) {
stuff();
}
}
The issue could be fixed technically by:
* Making the function use an actual stack of values instead of direct
register references. This would make function evaluation slower.
* Add a bytecode instruction to "wipe" any registers above a certain
index to ensure they contain no bogus references. These could be
issued after expression evaluation or in loop headers. This would
bloat bytecode.
Function closures are reference loops by default
------------------------------------------------
Function closures contain a reference loop by default::
var f = function() {};
print(f.prototype.constructor === f); // --> true
Unless user code explicitly sets a different ``f.prototype``, every
function closure requires a mark-and-sweep to be collected which makes
plain reference counting unattractive if there are a lot of function
temporaries. Such temporaries will then be reachable and only freed
when the heap is destroyed. This should be fixed in the future somehow
if possible.
Requirements for tracking heap allocated objects
------------------------------------------------
Mark-and-sweep only requires a single (forward) linked list to track
objects. Objects are inserted at the head, and scanned linearly
during mark and sweep. The sweep phase can remove an object by keeping
track of its predecessor when traversing the list. The same applies
to work lists.
Reference counting requires the ability to remove an arbitrarily
chosen object to be removed from the heap allocated list. To do
this efficiently, a double linked list is needed to avoid scanning
the list from the beginning.
Future work
===========
* During object property allocation resize, don't prevent compaction of
other objects in mark-and-sweep.
* Special handling for built-in strings and objects, so that they can be
allocated from a contiguous buffer, only freed when heap is freed.
* Incremental mark-and-sweep at least as an option in semi real-time
environments.
* Optimize reference count handling in performance critical code sections.
For instance:
- a primitive to INCREF a slice of tagged values would be useful
- often the target of an INCREF can be assumed to be non-NULL; a fast
path macro could assert for this but avoid otherwise checking for it
* Develop a fix for the function temporary register reachability issue.
* Develop a fix for function instance prototype reference loop issue.
* Add a figure of where objects may reside (string table, heap allocated,
refzero work list, mark-and-sweep to be finalized work list).