Lecture
Circular references occur when an array element contains a reference to its parent array, or when an object property contains a reference to the object itself. If a structure (array, object) contains a circular reference and that structure is deleted, then before PHP 5.3 the variable's memory was not actually freed, because the zvav value structure had a non-zero number in its refcount__gc field (the number of variables referencing the memory region). The circular reference pointed to the value being deleted, so the cell was considered in use, hence refcount__gc > 0, while freeing happens only for refcount__gc = 0. Because of this, free RAM was cluttered in an uncontrolled way and the memory leak problem arose.
PHP 5.3 introduced a circular reference collection mechanism that solves the memory leak problem.
The following code runs a loop of 30,001 steps, and on each step it creates an array whose second element references the parent array, creating a circular reference. The array is then destroyed with the unset function. At step zero and at every 2500th step after that, the amount of RAM used by the script is printed.
As we can see, memory first grows and then drops sharply. And this repeats several times.
This memory behavior is due to how circular reference collection works. When the refcount__gc reference counter is decremented (when a link to a reference is broken or a variable is deleted), the collector marks the potential circular structure zvav with a special buffer flag, which means the structure is in the root buffer. In the terminology of the collection algorithm, the contents of the buffer are called roots. Once the buffer fills up with 10,000 roots, the garbage collector is started. The buffer size is configured in the PHP source code in the GC_ROOT_BUFFER_MAX_ENTRIES constant.
While traversing the buffer, the collector checks whether a root is garbage or a live value that is still in use. The check is performed using the algorithm described in the paper "Concurrent Cycle Collection in Reference Counted Systems", presented in 2001 at the European Conference on Object-Oriented Programming.
Reference counting memory mechanisms, such as the one PHP used previously, usually do not solve the problem of memory leaks caused by circular references. Starting with version 5.3.0, PHP implements the synchronous algorithm from the paper "» Concurrent Cycle Collection in Reference Counted Systems", which addresses this issue.
A complete description of how the algorithm works is beyond the scope of this section, so only the basics are given. First, we need to establish a few basic rules. If a reference count is incremented, the container is still in use and is not garbage. If the count is decremented to zero, the zval can be freed. Based on these rules, memory leaks with circular references can only occur when a reference count is decremented to a non-zero value. Then, within the selected containers, garbage can be found by checking whether all reference counts can be decremented by one and identifying those containers whose count would become zero.

To avoid constantly checking for garbage with circular references every time a reference count is decremented, the algorithm adds all possible roots (zval containers) to a "root buffer" (marking them as "purple"). This also guarantees that any root gets into the buffer only once. The garbage collection mechanism starts only when the buffer fills up (see step A in the figure above).
In step B, the algorithm performs a depth-first search over all possible roots to decrement the reference count by one for all containers (marking them as "gray"). In step C, the algorithm performs a depth-first search again to check the reference counts. If it finds a count with a zero value, the container is marked as "white" (shown as blue in the figure). If the count is greater than zero, a depth-first search starts from that container, incrementing the counts back by one and re-marking their containers as "black". In the final step D, the algorithm walks through the root buffer and removes the container roots from it, while checking which containers are marked "white". These containers will be freed from memory.
Now that you have an idea of how the algorithm works, let us look at its integration into PHP. By default, the garbage collector is always enabled. To change this option, use the zend.enable_gc parameter in php.ini.
If the garbage collector is enabled, the circular reference search algorithm runs every time the root buffer fills up with 10,000 roots (you can change this value by modifying the GC_ROOT_BUFFER_MAX_ENTRIES constant in the Zend/zend_gc.c file in the PHP source code and rebuilding PHP). If the garbage collector is disabled, the algorithm will never run. However, roots are still always recorded in the buffer.
If the buffer fills up while the garbage collection mechanism is disabled, other roots will not be recorded in it. Thus, if they turn out to be garbage with circular references, they will never be cleaned up and will cause a memory leak.
The reason roots are always recorded in the buffer even when the garbage collection mechanism is disabled is that this is much faster than constantly checking whether the mechanism is enabled or not. However, garbage collection itself and its analysis algorithm can take a significant amount of time.
Besides changing the zend.enable_gc parameter, the garbage collection mechanism can also be started and stopped by calling the gc_enable() and gc_disable() functions, respectively. Calling these functions has the same effect as enabling/disabling the mechanism via configuration settings. In addition, you can start garbage collection even if the root buffer is not yet full. To do this, call the gc_collect_cycles() function, which also returns the number of circular references collected by the algorithm.
The reason for enabling and disabling the collection mechanism, as well as running it manually, may be that some parts of your application can be time-critical. In such cases you may not want the garbage collector to interfere. Of course, by disabling the garbage collector in certain places in your application you risk a memory leak, since some roots may potentially not fit into the limited root buffer. It is more sensible to call gc_collect_cycles() right before calling gc_disable() to free memory and the roots already recorded in the buffer. This will clear the buffer and leave more room for storing roots while the mechanism is disabled.
https://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf
https://www.php.net/manual/ru/features.gc.collecting-cycles.php
Comments