Що таке витоки пам’яті та як їх виправити?

Подібно до того, як мозок є життєво важливим для людини, так само і оперативна пам’ять є критичною для комп’ютерів. Без достатньої кількості оперативної пам’яті, ваша система не зможе виконувати поставлені завдання.

Нестача оперативної пам’яті та інші проблеми з її використанням можуть бути спричинені так званим “витоком пам’яті”. У цій статті ми розглянемо, як виявити та усунути ці витоки.

Проте, спочатку детальніше розберемося, що таке витік пам’яті та чому його потрібно виправляти.

Що ж таке витік пам’яті?

Уявіть собі парковку біля торговельного центру. Всі автомобілі припарковані там, незалежно від того, чи закінчили їхні власники свої покупки. З часом, на парковці не залишиться місця для нових авто, що призведе до заторів і зниження загальної ефективності торговельного центру.

Джерело зображення: prateeknima.medium.com

Подібна ситуація виникає і з комп’ютерами!

Програми, наче автомобілі на парковці, можуть “забувати” звільняти пам’ять, коли вона стає непотрібною. Це створює навантаження на пам’ять і перешкоджає нормальній роботі нових завдань, викликаючи типову проблему з пам’яттю, відому як “витік пам’яті”.

Розглянемо приклад коду, що демонструє витік пам’яті:

void memory_allocation() {
    int *ptr = (int*)malloc(sizeof(int));
}

Цей фрагмент коду на C виділяє пам’ять для цілочисельної змінної та зберігає її адресу в покажчику “ptr”. Але відсутній код, який би звільняв цю пам’ять, що і призводить до витоку.

def infinite_rec():
    return infinite_rec()

У наведеному вище коді на Python немає базового випадку для завершення рекурсивної функції. Це призводить до переповнення стеку та, як наслідок, до витоку пам’яті.

Основні причини витоку пам’яті

Неуважність програмістів

Головною причиною витоків пам’яті є неуважність програмістів.

Часто програмісти виділяють пам’ять для даних, але забувають її звільнити, коли вона більше не потрібна. З часом це заповнює всю доступну пам’ять, не залишаючи місця для нових завдань, що і призводить до витоку.

Мови програмування

Використання мов програмування без вбудованої системи керування пам’яттю може спричинити витоки.

Деякі мови, такі як Java, мають вбудовані “збирачі сміття”, які автоматично керують пам’яттю.

Інші, наприклад C++, не мають вбудованого збирача сміття. У цьому випадку програмісти повинні самостійно слідкувати за виділенням та звільненням пам’яті, що підвищує ризик витоків.

Активне використання кешу

Дані або програми, що часто використовуються, зберігаються у кеші для пришвидшення доступу до них.

Якщо елементи кешу не очищаються своєчасно, особливо якщо вони вже застарілі, це також може призвести до витоків пам’яті.

Використання глобальних змінних

Глобальні змінні утримують дані протягом усього часу роботи програми. Надмірне використання глобальних змінних призводить до тривалого використання пам’яті та, як наслідок, до витоків.

Неефективні структури даних

Розробники часто створюють власні структури даних для реалізації специфічних функцій. Помилки в управлінні пам’яттю цих структур можуть призводити до витоків.

Не закриті з’єднання

Незакриті файли, бази даних, мережеві з’єднання та інші ресурси після використання також можуть спричиняти витоки пам’яті.

Наслідки витоку пам’яті

Зниження продуктивності: З часом витоки пам’яті призводять до поступового зниження продуктивності програм або системи. Це відбувається через брак доступної пам’яті для нових завдань, що уповільнює роботу.

Аварійне завершення програм: Через постійне зростання витоків, програмам не вистачає пам’яті, що може призвести до їх аварійного завершення. Це, в свою чергу, може спричинити втрату даних і нестабільність системи.

Вразливості безпеки: Неналежне очищення конфіденційних даних, таких як паролі або особиста інформація, з пам’яті після використання, може зробити їх доступними для зловмисників під час витоку.

Вичерпання ресурсів: Програми, які страждають від витоків пам’яті, займають все більше місця в оперативній пам’яті. Це збільшує споживання ресурсів і знижує загальну продуктивність системи.

Як виявити витоки пам’яті?

Ручна перевірка коду

Перегляньте вихідний код, звертаючи увагу на випадки, коли пам’ять виділяється, але не звільняється після використання. Шукайте змінні та об’єкти, які використовують пам’ять, але не мають відповідного коду для її звільнення.

Також перевіряйте, як структури даних керують виділеною пам’яттю.

Статичний аналіз коду

Інструменти статичного аналізу сканують вихідний код та виявляють потенційні витоки пам’яті.

Вони використовують різні правила та шаблони, щоб виявити проблеми ще до виконання коду.

Інструменти динамічного аналізу

Ці інструменти аналізують код під час його виконання, відстежуючи використання пам’яті та виявляючи витоки.

Вони досліджують поведінку об’єктів та функцій в реальному часі та дозволяють більш точно ідентифікувати витоки.

Інструменти профілювання

Інструменти профілювання допомагають зрозуміти, як програма використовує пам’ять.

Розробники можуть аналізувати ці дані для оптимізації методів керування пам’яттю та запобігання проблемам з її витоками.

Бібліотеки для виявлення витоків

Деякі мови програмування мають вбудовані або сторонні бібліотеки, які допомагають виявляти витоки пам’яті.

Наприклад, Java має збирач сміття, а C++ пропонує CrtDbg. Також існують спеціалізовані бібліотеки, такі як LeakCanary, Valgrind, YourKit, які допомагають виявляти витоки у різних типах програм.

Як виправити витік пам’яті?

Ідентифікуйте витоки пам’яті

Перш за все, потрібно ідентифікувати витоки.

Використовуйте ручну перевірку коду, автоматизовані інструменти або інші методи, описані вище, щоб виявити, де саме програма втрачає пам’ять.

Визначте об’єкти, що викликають витік

Після того, як ви підтвердили наявність витоку, знайдіть конкретні об’єкти та структури даних, що його викликають. З’ясуйте, як саме вони використовують пам’ять та де саме відбувається помилка звільнення пам’яті.

Створіть тестові приклади

Створіть тестовий приклад, що відтворює витік, щоб переконатися, що ви правильно визначили його джерело. Також ці тести допоможуть перевірити, чи усунуто витік після внесення змін до коду.

Виправте код

Додайте код звільнення пам’яті для тих об’єктів, які виявлено як джерела витоку. Якщо код для звільнення пам’яті вже присутній, перевірте, чи працює він коректно.

Перевірте ще раз

Використайте інструменти виявлення витоків або автоматизовані тести, щоб переконатися, що витік пам’яті усунуто. Також перевірте загальну продуктивність програми та її функції, щоб переконатися, що внесені зміни не вплинули на них негативно.

Найкращі практики для запобігання витокам

Будьте відповідальним програмістом

Пам’ятайте про необхідність звільняти виділену пам’ять під час написання коду. Це мінімізує ризик виникнення витоків.

Згаданий раніше приклад:

void memory_allocation() {
    int *ptr = (int*)malloc(sizeof(int));
}

Виправимо його, додавши код звільнення пам’яті:

delete ptr;

Використовуйте мови програмування з вбудованим керуванням пам’яттю

Мови програмування, такі як Java чи Python, автоматично обробляють витоки пам’яті за допомогою вбудованих “збирачів сміття”.

Хоча це не виключає всіх можливих проблем, ці інструменти значно зменшують ризик витоків.

Тому рекомендується використовувати такі мови, де це можливо.

Уникайте циклічних посилань

Уникайте циклічних посилань у вашій програмі.

Циклічні посилання створюють ланцюжок, де об’єкти посилаються один на одного без кінця, що призводить до витоку. Наприклад, об’єкт A посилається на B, B на C, а C знову на A.

Зменшуйте використання глобальних змінних

Обмежте використання глобальних змінних, якщо вас турбує ефективність використання пам’яті. Глобальні змінні зберігають дані протягом усього часу роботи програми, що не є гарною практикою в управлінні пам’яттю.

Надавайте перевагу локальним змінним. Вони більш ефективні, оскільки пам’ять, виділена для них, звільняється після завершення виклику функції.

Приклад глобальної змінної:

int x = 5; // Глобальна змінна
void func(){
    print(x);
}

А приклад локальної змінної:

void func(){
    int x = 5; // Локальна змінна
    print(x);
}

Обмежте використання кешу

Встановіть обмеження на об’єм пам’яті, який може використовувати кеш. Іноді всі завдання, що виконуються в системі, надсилаються до кешу, що може призвести до його переповнення та, як наслідок, до витоку пам’яті.

Встановлення обмежень на розмір кешу допоможе запобігти цим проблемам.

Ретельно тестуйте

Обов’язково включіть тести на витік пам’яті на етапі тестування.

Створюйте автоматизовані тести, що охоплюють всі можливі сценарії, щоб виявити витоки до випуску коду.

Використовуйте інструменти моніторингу

Використовуйте автоматичні інструменти профілювання для моніторингу використання пам’яті. Регулярне відстеження допоможе вам виявити потенційні витоки на ранніх стадіях.

Visual Studio Profiler, NET Memory Profiler та JProfiler – це декілька корисних інструментів для цього.

Висновок

Ефективне керування пам’яттю є критично важливим для досягнення максимальної продуктивності програми. Витоки пам’яті не можна ігнорувати. Їх необхідно усувати та запобігати їхній появі в майбутньому. У цій статті ми розглянули, як це робити.

Ми показали вам різні методи виявлення витоків пам’яті, кроки для їх виправлення, а також найкращі практики, яких варто дотримуватись для уникнення їх в майбутньому.

Наступним кроком для вас може стати вивчення того, як виправити помилку “недостатньо пам’яті” в Windows.