В поисках утечек памяти
Шкарбатов Дмитрий
Руковожу командой web-разработки
Проекты: Касса, ПриватМаркет, Международные переводы, ...
shkarbatov@gmail.com
Утeчка памяти (англ. memory leak) — процесс неконтролируемого уменьшения объёма свободной памяти компьютера, связанный с ошибками в работающих программах, вовремя не освобождающих ненужные уже участки памяти, или с ошибками системных служб контроля памяти.
Утечки памяти обычно не беспокоят PHP-разработчиков. Типичное приложение обрабатывает один запрос и работает не больше секунды. После этого вся использованная им память освобождается.
Даже если приложение кушает слишком много, максимум, разработчик упирается в memory_limit, что решить в общем случае довольно просто: как только переменная становится не нужна, очищаем память, занимаемую ей, при помощи unset.
Основными последствиями утечки памяти являются:
Если вы разрабатываете ресурсоёмкие задачи (например, обработки большого количества данных) или стоит вопрос запуска PHP как демона, то проблема утечек встаёт очень остро.
PHP предназначен для работы не более 1 сек.
Для длительных процессов нужно использовать Python/Java/Rust/Go.
PHP рожден что бы умереть!
Zval структура в PHP 5
объявляется следующим образом:
typedef struct _zval_struct {
zvalue_value value;
zend_uint refcount__gc;
zend_uchar type;
zend_uchar is_ref__gc;
} zval;
Создание array zval
Если у вас установлен » Xdebug, то вы можете вывести структуру переменной,
вызвав функцию xdebug_debug_zval().
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
Результатом выполнения данного примера будет следующее:
a: (refcount=1, is_ref=0)=array (
'meaning' => (refcount=1, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42
)
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет следующее:
a: (refcount=1, is_ref=0)=array (
'meaning' => (refcount=2, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42,
'life' => (refcount=2, is_ref=0)='life'
)
Добавление уже существующего элемента в массив
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет следующее:
a: (refcount=1, is_ref=0)=array (
'life' => (refcount=1, is_ref=0)='life'
)
Удаление элемента из массива
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );
?>
Результатом выполнения данного примера будет следующее:
a: (refcount=2, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
Добавление массива новым элементом в самого себя
<?php
unset($a);
?>
Результатом выполнения данного примера будет следующее:
a: (refcount=1, is_ref=1)=array (
0 => (refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)
Удаление $a
До PHP 5.3 - никак
xdebug_start_gcstats() - Начало сбора статистики GC
xdebug_stop_gcstats() - Конец сбора статистики GC
// ==================================
Как отдельный php-модуль:
https://github.com/tideways/php_garbage_stats
// ==================================
Является частью Xdebug с версии 2.6
<?php
class MyClass{}
function buildObjects() {
$c = new MyClass();
$b = new MyClass();
$c->b = $b;
}
$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
buildObjects();
}
// ==================================
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php
Found 0 garbage collection runs in current script.
Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------
<?php
class MyClass{}
function buildObjects() {
$c = new MyClass();
$b = new MyClass();
$c->b = $b;
$b->c = $c;
}
$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
buildObjects();
}
// ==================================
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php
Found 6 garbage collection runs in current script.
Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------
9998 | 99.98 % | 0.81 ms | 4646232 | 487560 | 89.51 % | buildObjects
10000 | 100.00 % | 0.63 ms | 4648848 | 489344 | 89.47 % | buildObjects
10000 | 100.00 % | 1.11 ms | 4650312 | 490808 | 89.45 % | buildObjects
10000 | 100.00 % | 1.28 ms | 4651776 | 492272 | 89.42 % | buildObjects
10000 | 100.00 % | 0.80 ms | 4653240 | 493736 | 89.39 % | buildObjects
10000 | 100.00 % | 0.69 ms | 4654704 | 495200 | 89.36 % | buildObjects
<?php
class MyClass{}
function buildObjects() {
$c = new MyClass();
$b = new MyClass();
$c->b = $b;
$b->c = $c;
}
$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
buildObjects();
if ($i%3000 == 0)
echo number_format(memory_get_usage()) . "\n";
}
// ==================================
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php
365,080
2,918,456
1,322,608
3,818,608
2,156,072
493,536
2,989,536
1,327,000
3,823,000
2,160,464
<?php
class MyClass{}
function buildObjects() {
$c = new MyClass();
$b = new MyClass();
$c->b = $b;
$b->c = $c;
return [$c, $b];
}
$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
$c = buildObjects();
unset($c[0]->b);
}
// ==================================
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php
365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832
365,832
<?php
class MyClass{}
function buildObjects() {
$c = new MyClass();
$b = new MyClass();
$c->b = $b;
$b->c = $c;
return [$c, $b];
}
$leakHolder = [];
for ($i = 0; $i < 30000; $i++) {
$c = buildObjects();
unset($c[0]->b);
}
// ==================================
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 test.php
Found 0 garbage collection runs in current script.
Collected | Efficency% | Duration | Memory Before | Memory After | Reduction% | Function
----------|------------|----------|---------------|--------------|------------|---------
Если переменная вне области действия и больше не используется в каком-либо другом месте кода, то сборка мусора будет произведена автоматически. Вы можете принудительно выполнить это раньше, используя метод unset() для выведения переменной из области действия.
Если переменная является частью циклической ссылки, то она может быть очищена только посредством PHPs cycle garbage collector. Он запускается всякий раз, когда 10000 возможных циклических объектов или массивов на данный момент в памяти.
Если вы вызываете функцию gc_collect_cycles().
memory_get_usage() — возвращает количество памяти, выделенное для PHP на момент запуска скрипта.
memory_get_peak_usage() — возвращает пиковое значение объема памяти, выделенное PHP.
gc_collect_cycles() — принудительный запуск сборщика мусора.
XHProf
+
php-meminfo
<?php
// Инициализируем профайлер
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
class MyClassA {
private $myObjectName;
public function __construct($name)
{
$this->myObjectName = $name;
}
};
$myString = "My very nice string";
$myFloat = 3.14;
$myInt = 42;
$myNull = null;
$myRootArray = [];
for($i = 0; $i < 100000; $i++) {
$myRootArray['first level']['second level'][] = new MyClassA('Object '.$i);
};
// Останавливаем профайлер после выполнения программы
$xhprof_data = xhprof_disable();
include_once "/var/www/xhprof-0.9.4/xhprof_lib/utils/xhprof_lib.php";
include_once "/var/www/xhprof-0.9.4/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($xhprof_data, "test");
Для PHP7 стоит использовать не родной XHProf, а его форк, так как в стандартном расширении нет всех метрик.
https://github.com/phacility/xhprof/issues/82
<?php
class MyClassA {
private $myObjectName;
public function __construct($name)
{
$this->myObjectName = $name;
}
};
$myString = "My very nice string";
$myFloat = 3.14;
$myInt = 42;
$myNull = null;
$myRootArray = [];
for($i = 0; $i < 100; $i++) {
$myRootArray['first level']['second level'][] = new MyClassA('Object '.$i);
};
gc_collect_cycles();
meminfo_dump(fopen('/tmp/php_mem_dump100.json','w'));
$ analyzer/bin/analyzer summary /tmp/php_mem_dump100.json
+----------+-----------------+-----------------------------+
| Type | Instances Count | Cumulated Self Size (bytes) |
+----------+-----------------+-----------------------------+
| string | 183 | 8486 |
| MyClassA | 100 | 7200 |
| array | 12 | 864 |
| integer | 5 | 80 |
| float | 2 | 32 |
| null | 1 | 16 |
+----------+-----------------+-----------------------------+
$ analyzer/bin/analyzer query -v -f "class=MyClassA" -f "is_root=0" /tmp/php_mem_dump100.json
+----------------+-------------------+------------------------------+
| Item ids | Item data | Children |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f008 | Type: object | myObjectName: 0x7f4d5c85cde0 |
| | Class: MyClassA | |
| | Object Handle: 1 | |
| | Size: 72 B | |
| | Is root: No | |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f028 | Type: object | myObjectName: 0x7f4d5c85cf20 |
| | Class: MyClassA | |
| | Object Handle: 2 | |
| | Size: 72 B | |
| | Is root: No | |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f048 | Type: object | myObjectName: 0x7f4d5c85cca0 |
| | Class: MyClassA | |
| | Object Handle: 3 | |
| | Size: 72 B | |
| | Is root: No | |
+----------------+-------------------+------------------------------+
| 0x7f4d5c87f068 | Type: object | myObjectName: 0x7f4d5c85d060 |
...
$ analyzer/bin/analyzer -v ref-path 0x7f4d5c87f008 /tmp/php_mem_dump100.json
Found 1 paths
Path from 0x7f4d5c8562a0
+--------------------+
| Id: 0x7f4d5c87f008 |
| Type: object |
| Class: MyClassA |
| Object Handle: 1 |
| Size: 72 B |
| Is root: No |
| Children count: 1 |
+--------------------+
^
|
0
|
|
+---------------------+
| Id: 0x7f4d5c85cb60 |
| Type: array |
| Size: 72 B |
| Is root: No |
| Children count: 100 |
+---------------------+
^
|
second level
|
^
|
second level
|
|
+--------------------+
| Id: 0x7f4d5c85ca20 |
| Type: array |
| Size: 72 B |
| Is root: No |
| Children count: 1 |
+--------------------+
^
|
first level
|
|
+---------------------------+
| Id: 0x7f4d5c8562a0 |
| Type: array |
| Size: 72 B |
| Is root: Yes |
| Execution Frame: <GLOBAL> |
| Symbol Name: myRootArray |
| Children count: 1 |
+---------------------------+
Причина, по которой наш объект все еще находится в памяти, состоит в том, что он находится в массиве, который сам по себе находится в другом массиве, который находится в конечном массиве. И последний массив напрямую связан с переменной, объявленной в <GLOBAL>, и называется myRootArray.
В более короткой нотации PHP это может быть написано так:
$myRootArray ['first level'] ['second level'] [0] = 0x7f4d5c87f008;
$ php -dgc_stats.enable=1 -dgc_stats.show_report=1 bin/composer update
Found 157 garbage collection runs in current script.
Collected | Efficency% | Duration | Reduction% | Function
----------|------------|----------|------------|---------
796 | 7.96 % | 2.59 ms | 0.63 % | [..]::loadProviderListings
0 | 0.00 % | 0.91 ms | -0.00 % | [..]::loadProviderListings
0 | 0.00 % | 17.19 ms | -0.01 % | [..]::parseConstraints
0 | 0.00 % | 19.95 ms | -0.03 % | ArrayLoader::load
0 | 0.00 % | 22.36 ms | -0.02 % | Pool::computeWhatProvides
0 | 0.00 % | 30.40 ms | -0.01 % | ArrayLoader::parseLinks
0 | 0.00 % | 29.05 ms | -0.00 % | Rule2Literals::equals
0 | 0.00 % | 29.00 ms | -0.01 % | [..]::createRule2Literals
0 | 0.00 % | 32.90 ms | -0.09 % | RuleWatchNode::__construct
0 | 0.00 % | 35.08 ms | -0.09 % | Solver::solve
0 | 0.00 % | 44.10 ms | -0.09 % | makeAssertionRuleDecisions
0 | 0.00 % | 53.28 ms | -0.01 % | Solver::runSat
0 | 0.00 % | 24.21 ms | -0.00 % | Transaction::findUpdates
183 | 1.83 % | 31.34 ms | 0.01 % | Installer::doInstall
0 | 0.00 % | 24.10 ms | -0.00 % | Installer::doInstall
// Более детальное описание
https://blog.ircmaxell.com/2014/12/what-about-garbage.html
Переносите существующие обработчики на PHP7, там PHP много что (не все), подумает за вас;
Возможно в некоторых случаях стоит пересмотреть работу GC в сторону его отключения.