Распространенные причины утечек памяти в JavaScript¶
Выявление и устранение распространенных утечек памяти JavaScript (Node.js и Deno.js)
Утечки памяти - это тихая угроза, которая постепенно снижает производительность, приводит к сбоям и увеличивает эксплуатационные расходы. В отличие от очевидных ошибок, утечки памяти часто незаметны, и их трудно заметить, пока они не начнут вызывать серьезные проблемы.
Повышенное потребление памяти приводит к увеличению затрат на сервер и негативно сказывается на удобстве работы пользователей. Понимание того, как возникают утечки памяти, - первый шаг к их устранению.
Утечка памяти происходит, когда ваше приложение выделяет память, а затем не освобождает ее после того, как она больше не нужна. Со временем эти неосвобожденные блоки памяти накапливаются, что приводит к постепенному увеличению потребления памяти.
Это особенно проблематично для длительно работающих процессов, таких как веб-серверы, где утечка может привести к тому, что приложение будет потреблять все больше и больше памяти, пока в конце концов не произойдет сбой или оно не замедлится до ползучего состояния.
External: Память, используемая объектами C++, связанными с JavaScript.¶
Внешняя память - это память, используемая объектами C++, связанными с JavaScript. Эти объекты создаются с помощью привязок, которые позволяют JavaScript взаимодействовать с нативным кодом, выделяя память за пределами типичной кучи JavaScript.
Эта память не видна непосредственно в JavaScript, но все равно увеличивает общее количество памяти, используемой приложением.
Метод Buffer.alloc выделяет буфер размером 50 МБ, который отслеживается как внешняя память.
external.js
12345678
constbuffer=Buffer.alloc(50*1024*1024);// Allocate 50MB of bufferconsole.log('Initial Memory Usage:',process.memoryUsage());setInterval(()=>{constmemoryUsage=process.memoryUsage();console.log(`External Memory: ${memoryUsage.external}`);},1000);
В этом примере регистрируется использование внешней памяти, которая будет отражать распределение буфера.
Array Buffers: Память, выделенная для объектов ArrayBuffer¶
Буферы массивов - это память, используемая для объектов ArrayBuffer. Эти объекты хранят двоичные данные фиксированной длины в JavaScript.
ArrayBuffer является частью системы типизированных массивов JavaScript, позволяя вам работать с двоичными данными напрямую.
Память для этих буферов отслеживается отдельно от обычных объектов JavaScript. Они часто используются для работы с необработанными данными, такими как файлы или сетевые протоколы.
Вот пример, в котором я выделяю ArrayBuffer размером 50 МБ, а затем проверяю начальное использование памяти моим процессом Node.js.
Неправильное управление переменными может привести к утечке памяти.
Например, если вы объявите переменные, которые должны быть временными, но забудете их очистить, они будут продолжать потреблять память.
1 2 3 4 5 6 7 8 91011
letcache={};functionstoreData(key,value){cache[key]=value;}// Simulating the function being called multiple timesstoreData('item1',newArray(1000000).fill('A'));storeData('item2',newArray(1000000).fill('B'));// Memory leak: data stored in 'cache' is never released
В приведенном выше примере данные добавляются в глобальный объект под названием cache. Если эти данные не удалять, когда они больше не нужны, они будут продолжать неоправданно использовать память.
Это особенно проблематично, если эти переменные хранятся в глобальной области видимости, что позволяет сохранять их в течение всего жизненного цикла приложения.
1 2 3 4 5 6 7 8 9101112131415161718192021222324
letglobalUserSessions={};// Global scopefunctionaddUserSession(sessionId,userData){// Store user data in global scopeglobalUserSessions[sessionId]=userData;}functionremoveUserSession(sessionId){// Manually remove user sessiondeleteglobalUserSessions[sessionId];}// Simulate adding user sessionsaddUserSession('session1',{name:'Alice',data:newArray(1000000).fill('A'),});addUserSession('session2',{name:'Bob',data:newArray(1000000).fill('B'),});// The globalUserSessions object will persist// for the entire app lifecycle unless manually cleaned up
globalUserSessions - это глобальный объект, используемый для хранения данных пользовательских сессий. Поскольку он находится в глобальной области видимости, он сохраняется в течение всего времени выполнения приложения.
Если сессии не удалены должным образом с помощью removeUserSession, данные останутся в памяти на неопределенный срок, что приведет к утечке памяти.
Глобальные объекты могут занимать память дольше, чем это необходимо. Данные в них могут оставаться в памяти после того, как они больше не нужны. Это постепенно увеличивает использование памяти.
12345
global.config={settings:newArray(1000000).fill('Configuration'),};// Memory leak: 'config' is global and remains// in memory for the entire application lifecycle
Поскольку config имеет глобальный доступ и никогда не очищается, используемая им память сохраняется в течение всего времени работы приложения. Вот один из способов избежать утечки памяти:
1 2 3 4 5 6 7 8 910111213141516
functioncreateConfig(){return{settings:newArray(1000000).fill('Configuration'),};}// Use config only when needed, and let it be garbage collected afterwardsfunctionprocessConfig(){constconfig=createConfig();// Perform operations with configconsole.log(config.settings[0]);// Config will be cleared from memory once it's no longer referenced}processConfig();
Вместо того чтобы хранить config в глобальном объекте, мы храним config локально внутри функции. Это гарантирует, что config будет очищена после выполнения функции, освобождая память для сборки мусора.
Каждую секунду добавляется новый слушатель событий. Однако эти слушатели никогда не удаляются, что приводит к их накоплению в памяти.
Каждый слушатель хранит ссылку на функцию listener и все связанные с ней переменные, что препятствует сборке мусора и со временем приводит к увеличению использования памяти.
Чтобы предотвратить эту утечку памяти, следует удалять слушатели событий, когда они больше не нужны.
1 2 3 4 5 6 7 8 91011121314151617
constEventEmitter=require('events');constmyEmitter=newEventEmitter();functionlistener(){console.log('Event triggered!');}// Add an event listenermyEmitter.on('event',listener);// Trigger the event and then remove the listenermyEmitter.emit('event');myEmitter.removeListener('event',listener);// Alternatively, you can use `once` method to add a listener// that automatically removes itself after being triggeredmyEmitter.once('event',listener);
Замыкания в JavaScript могут непреднамеренно удерживать переменные дольше, чем это необходимо. Когда закрытие захватывает переменную, оно сохраняет ссылку на нее в памяти.
Если закрытие используется в долго выполняющемся процессе или не завершено должным образом, захваченные переменные остаются в памяти, что приводит к утечке.
1 2 3 4 5 6 7 8 910
functioncreateClosure(){letcapturedVar=newArray(1000000).fill('Data');returnfunction(){console.log(capturedVar[0]);};}constclosure=createClosure();// The closure holds onto 'capturedVar', even if it's not used anymore.
Чтобы избежать утечек, убедитесь, что закрытия не захватывают без необходимости большие переменные и не завершают их, когда они больше не нужны.
1 2 3 4 5 6 7 8 910111213
functioncreateClosure(){letcapturedVar=newArray(1000000).fill('Data');returnfunction(){console.log(capturedVar[0]);// Release memory when no longer neededcapturedVar=null;};}constclosure=createClosure();// 'capturedVar' is released after use.closure();
В некоторых сценариях неуправляемые обратные вызовы могут вызывать проблемы с памятью, если они удерживают переменные или объекты дольше, чем это необходимо.
Однако сборщик мусора JavaScript обычно эффективно очищает память, когда ссылки больше не нужны.
1 2 3 4 5 6 7 8 91011121314
functionfetchData(callback){letdata=newArray(1000000).fill('Data');setTimeout(()=>{callback(data);},1000);}functionhandleData(data){console.log(data[0]);}// The 'data' array remains in memory.fetchData(handleData);
В приведенном выше примере:
Распределение данных: Функция fetchData выделяет большой массив (data), который содержит 1 миллион элементов.
Ссылка на обратный вызов: Функция обратного вызова handleData ссылается на этот большой массив, когда она вызывается функцией setTimeout через 1 секунду. Несмотря на большое распределение, сборщик мусора JavaScript гарантирует, что память будет освобождена, когда она больше не нужна.
Нет необходимости вручную очищать ссылки, если только вы не имеете дело с очень сложными сценариями, в которых ссылки непреднамеренно сохраняются.
functionfetchData(callback){letdata=newArray(1000000).fill('Data');setTimeout(()=>{callback(data);data=null;// Release the referenceglobal.gc();// Explicitly trigger garbage collection},1000);}functionhandleData(data){console.log(data[0]);data=null;// Clear reference after handling}console.log('Initial Memory Usage:',process.memoryUsage());fetchData(handleData);setTimeout(()=>{console.log('Final Memory Usage:',process.memoryUsage());},2000);// Give some time for garbage collection
Хотя этот код вручную очищает ссылки и явно запускает сборку мусора, он вносит ненужные сложности.
Сборщик мусора JavaScript обычно справляется с очисткой памяти без этих дополнительных действий.
В большинстве сценариев такое ручное вмешательство не только излишне, но и может усложнить сопровождение кода.
Использование bind() создает новую функцию с ключевым словом this, установленным на определенное значение. Если вы не будете осторожны, это может привести к утечке памяти.
1 2 3 4 5 6 7 8 9101112131415
functionMyClass(){this.largeData=newArray(1000000).fill('leak');window.addEventListener('click',this.handleClick.bind(this));}MyClass.prototype.handleClick=function(){console.log('Clicked');};// If MyClass instance is destroyed, but the event listener is not removed,// the bound function will keep the instance alive in memory.
Почему происходят утечки памяти при использовании bind()¶
Ссылки сохраняются: Когда вы используете bind(), новая функция запоминает исходную функцию и это значение. Если вы не удалите функцию, когда она больше не нужна, она останется и будет использовать память.
Большие объекты остаются в памяти: Связанные функции могут случайно оставить в памяти большие объекты, даже если они вам больше не нужны.
Циклические ссылки возникают, когда два объекта ссылаются друг на друга. Это создает цикл, который может запутать сборщик мусора, не позволяя ему освободить память.
123456
functionCircularReference(){this.reference=this;// Circular reference}letobj=newCircularReference();obj=null;// Setting obj to null may not free the memory.
Даже если вы установите obj в null, память может быть не освобождена из-за самоцикла. Вот как можно избежать круговой ссылки.
Разорвите цикл: Убедитесь, что объекты не ссылаются друг на друга, когда они больше не нужны. Это поможет сборщику мусора очистить их.
123456789
functionCircularReference(){this.reference=this;}letobj=newCircularReference();// Breaking the circular referenceobj.reference=null;obj=null;// Now the memory can be freed
Установив для obj.reference значение null, мы разрываем круговую ссылку. Это позволит сборщику мусора освободить память, когда obj больше не нужен.
Использование слабых ссылок: Использование WeakMap, WeakSet или WeakRef позволяет сборщику мусора очищать память даже при наличии ссылок, если они слабые.
1 2 3 4 5 6 7 8 910
letweakMap=newWeakMap();functionCircularReference(){letobj={};weakMap.set(obj,'This is a weak reference');returnobj;}letobj=CircularReference();// The object can be garbage collected when no longer needed
В weakMap хранится слабая ссылка на obj. Это означает, что когда obj больше не используется в других местах, он все равно может быть собран в мусор, даже если на него ссылается weakMap.
1 2 3 4 5 6 7 8 910111213
letweakRef;functioncreateObject(){letobj={data:'important'};weakRef=newWeakRef(obj);returnobj;}letobj=createObject();console.log(weakRef.deref());// { data: 'important' }obj=null;// Now the object can be garbage collected
weakRef позволяет хранить слабую ссылку на obj. Если obj установлен в null и на него нет других ссылок, он может быть собран в мусор, даже если weakRef все еще существует.
WeakMap, WeakSet и WeakRef отлично подходят для предотвращения утечек памяти, но они могут не понадобиться вам постоянно. Они больше подходят для продвинутых случаев использования, таких как управление кэшем или большими данными.
Если вы работаете над типичными веб-приложениями, то, возможно, не часто будете с ними сталкиваться, но полезно знать, что они существуют, когда вам это нужно.
Чтобы найти утечки памяти, вам нужно профилировать ваше приложение, чтобы понять, как используется память.
Вот приложение Node.js, созданное для имитации задач, требующих больших затрат процессора, операций ввода-вывода и намеренного создания утечки памяти в целях тестирования.
consthttp=require('http');consturl=require('url');// Simulate a CPU-intensive taskconsthandleCpuIntensiveTask=(req,res)=>{letresult=0;for(leti=0;i<1e7;i++){result+=i*Math.random();}console.log('Memory Usage (CPU Task):',process.memoryUsage());// Log memory usageres.writeHead(200,{'Content-Type':'text/plain'});res.end(`Result of the CPU-intensive task: ${result}`);};// Create a large in-memory buffer// 50MB buffer filled with 'a'constlargeBuffer=Buffer.alloc(1024*1024*50,'a');// Simulate an I/O operationconsthandleSimulateIo=(req,res)=>{// Simulate reading the buffer as if it were a filesetTimeout(()=>{console.log('Memory Usage (Simulate I/O):',process.memoryUsage());// Log memory usageres.writeHead(200,{'Content-Type':'text/plain',});res.end(`Simulated I/O operation completed with data of length: ${largeBuffer.length}`);},500);// Simulate a 500ms I/O operation};// Simulate a memory leak (For Testing)letmemoryLeakArray=[];constcauseMemoryLeak=()=>{memoryLeakArray.push(newArray(1000).fill('memory leak'));console.log('Memory leak array length:',memoryLeakArray.length);};constserver=http.createServer((req,res)=>{constparsedUrl=url.parse(req.url,true);if(parsedUrl.pathname==='/cpu-intensive'){handleCpuIntensiveTask(req,res);}elseif(parsedUrl.pathname==='/simulate-io'){handleSimulateIo(req,res);}elseif(parsedUrl.pathname==='/cause-memory-leak'){causeMemoryLeak();res.writeHead(200,{'Content-Type':'text/plain',});res.end('Memory leak caused. Check memory usage.');}else{res.writeHead(404,{'Content-Type':'text/plain',});res.end('Not Found');}});constPORT=process.env.PORT||3000;server.listen(PORT,()=>{console.log(`Server is running on port ${PORT}`);});
Далее нам нужно провести стресс-тестирование нашего сервера. Этот скрипт проводит стресс-тестирование сервера, отправляя по 100 запросов для имитации утечек процессора, ввода-вывода и памяти.
#!/bin/bash# Number of requests to sendREQUESTS=100# Endpoint URLsCPU_INTENSIVE_URL="http://localhost:3000/cpu-intensive"SIMULATE_IO_URL="http://localhost:3000/simulate-io"MEMORY_LEAK_URL="http://localhost:3000/cause-memory-leak"echo"Sending $REQUESTS requests to $CPU_INTENSIVE_URL and $SIMULATE_IO_URL..."# Loop for CPU-intensive endpointfor((i=1;i<=REQUESTS;i++));docurl-s$CPU_INTENSIVE_URL>/dev/null&done# Loop for Simulated I/O endpointfor((i=1;i<=REQUESTS;i++));docurl-s$SIMULATE_IO_URL>/dev/null&done# Loop for Memory Leak endpointfor((i=1;i<=REQUESTS;i++));docurl-s$MEMORY_LEAK_URL>/dev/null&donewaitecho"Done."
Он перебирает URL-адреса и отправляет тихие запросы с помощью curl, выполняя их в фоновом режиме, чтобы имитировать высокую нагрузку.
В результате будет создан файл processed-profile.txt с данными профилирования процессора, которые содержат подробную информацию о том, где ваше приложение проводило время и как оно управляло памятью.
Откройте файл processed-profile.txt и найдите области, где используется значительное количество времени или памяти.
Функции с высоким использованием процессора: Это самые узкие места в вашем коде.
Функции, потребляющие много памяти: Функции, потребляющие большое количество памяти, могут указывать на потенциальные утечки памяти, особенно если они соответствуют частям вашего кода, которые должны освобождать память, но не делают этого.
Петля событий и сборка мусора (GC): Ищите высокий процент времени, проведенного в GC, так как это может свидетельствовать о том, что приложение испытывает трудности с управлением памятью. Утечки памяти могут быть незаметными, но их устранение является ключевым фактором для обеспечения эффективности и надежности ваших JavaScript-приложений.