JavaRush /Курси /JAVA 25 SELF /Профілювання та оптимізація коду: інструменти, підходи

Профілювання та оптимізація коду: інструменти, підходи

JAVA 25 SELF
Рівень 63 , Лекція 4
Відкрита

1. Вступ до профілювання

Профілювання — це як медичне обстеження для вашого застосунку: ми не просто дивимося на «температуру» (моніторинг), а шукаємо, де у застосунку «болить», що працює повільно, де витрачається надто багато пам’яті або ресурсів.

Профілювання — це процес збирання та аналізу інформації про роботу програми з метою виявлення вузьких місць (bottlenecks) і неефективних ділянок коду. На відміну від моніторингу, який зазвичай відстежує загальні показники (навантаження на процесор, пам’ять, кількість потоків), профілювання дає змогу зазирнути всередину: дізнатися, які методи викликаються найчастіше, скільки часу вони займають, скільки об’єктів створюється і де саме відбувається витік пам’яті.

Коли профілювання справді потрібне?

  • Застосунок «гальмує», але незрозуміло, чому.
  • Раптово зросло споживання пам’яті.
  • Після оновлення коду щось стало працювати довше.
  • Потрібно зрозуміти, чому серверу бракує ресурсів.

До речі, майже кожен розробник хоча б раз у житті оптимізував не ту ділянку коду. Чому? Тому що «на око» визначити вузьке місце практично неможливо — для цього і потрібен профайлер.

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

  • Час виконання методів (CPU‑профілювання): Які методи займають найбільше часу? Де програма «витрачає» процесор?
  • Використання пам’яті (Memory‑профілювання): Які об’єкти створюються найчастіше? Де вони лишаються в пам’яті довше, ніж потрібно?
  • Кількість об’єктів: Чи не створюємо ми надто багато однотипних об’єктів?
  • Потоки: Чи не надто багато потоків? Чи немає блокувань (deadlock, contention)?
  • Виклики методів: Яка глибина стеку? Чи не відбувається рекурсія без виходу?

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

У світі Java є кілька класичних (і безкоштовних!) інструментів, які дозволяють виконувати профілювання. Розглянемо основні з них.

VisualVM

VisualVM — це безкоштовний інструмент, який входить до складу JDK (починаючи з JDK 6). Він дозволяє:

  • Під’єднуватися до локальних і віддалених JVM.
  • Переглядати пам’ять, потоки, CPU, збирання сміття.
  • Робити heap dump і аналізувати його.
  • Профілювати застосунок за CPU і пам’яттю.

Як запустити VisualVM?
Зазвичай він знаходиться в теці JDK: <шлях_до_JDK>/bin/jvisualvm
Запускаєте, обираєте процес Java‑застосунку — і можна спостерігати за його життям, як за рибками в акваріумі (тільки рибки тут — це об’єкти та потоки).

JProfiler, YourKit

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

  • Профілювати пам’ять, CPU, потоки.
  • Робити аналіз «знімків» пам’яті (heap dump).
  • Шукати витоки, тривалі блокування, повільні методи.
  • Інтегруватися з IDE та CI/CD.

Для початку вистачить VisualVM, але якщо ви «доростете» до великих проєктів — придивіться до цих інструментів.

Java Flight Recorder (JFR)

JFR — це вбудований у JDK інструмент для збирання подій про роботу JVM. Він дуже легкий, майже не впливає на продуктивність, і дозволяє збирати інформацію про:

  • Час роботи методів.
  • Збирання сміття.
  • Потоки, блокування, помилки.

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

3. Практика: профілюємо простий застосунок

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

Приклад коду: «Повільний калькулятор»

import java.util.ArrayList;
import java.util.List;

public class SlowCalculator {
    private final List<String> history = new ArrayList<>();

    public int add(int a, int b) {
        simulateHeavyOperation();
        int result = a + b;
        history.add(a + " + " + b + " = " + result);
        return result;
    }

    public int multiply(int a, int b) {
        simulateHeavyOperation();
        int result = a * b;
        history.add(a + " * " + b + " = " + result);
        return result;
    }

    public List<String> getHistory() {
        return history;
    }

    // Симуляція "важкої" операції
    private void simulateHeavyOperation() {
        for (int i = 0; i < 5_000_000; i++) {
            Math.sqrt(i);
        }
    }
}

А тепер — основний клас:

public class Main {
    public static void main(String[] args) {
        SlowCalculator calc = new SlowCalculator();
        for (int i = 0; i < 10; i++) {
            calc.add(i, i * 2);
            calc.multiply(i, i + 5);
        }
        System.out.println("Історія операцій:");
        for (String entry : calc.getHistory()) {
            System.out.println(entry);
        }
    }
}

Як профілювати цей застосунок?

  1. Скомпілюйте та запустіть застосунок.
  2. Відкрийте VisualVM (jvisualvm).
  3. Знайдіть свій процес (зазвичай за назвою класу Main).
  4. Перейдіть на вкладку CPU Profiler і натисніть Start.
  5. Дайте програмі попрацювати (або запустіть обчислення повторно).
  6. Подивіться, які методи займають найбільше часу.

Запитання: Який метод, на вашу думку, буде най«важчим»?
Відповідь: Звісно ж, simulateHeavyOperation() — адже він виконує величезний цикл на 5_000_000 ітерацій і викликає Math.sqrt.

4. Типові проблеми продуктивності

Повільні алгоритми

Найчастіша причина: невдалий вибір алгоритму або структури даних. Наприклад, пошук у списку замість використання HashMap, або сортування «бульбашкою» замість швидкого сортування.

Приклад:

// Повільний пошук
for (String s : list) {
    if (s.equals("target")) {
        // знайшли
    }
}

Краще використовувати Set або Map для швидкого пошуку.

Витоки пам’яті

Витік пам’яті — це ситуація, коли об’єкти залишаються «живими» (на них є посилання), хоча вони більше не потрібні. Це призводить до зростання споживання пам’яті і, зрештою, до OutOfMemoryError.

public class MemoryLeakDemo {
    private static List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        while (true) {
            leakyList.add(new byte[1_000_000]); // 1 МБ
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        }
    }
}

Як знаходити витоки?
Зробіть heap dump у VisualVM і подивіться, які об’єкти займають найбільше пам’яті та чому на них є посилання.

Надмірне створення об’єктів

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

for (int i = 0; i < 1_000_000; i++) {
    String s = new String("hello"); // погано!
}

Краще використовувати константи або пул рядків (String pool).

Блокування потоків

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

public synchronized void doWork() {
    // ...
}

Як виявляти?
На вкладці Threads у VisualVM можна побачити, які потоки блокуються або очікують — і чому.

5. Підходи до оптимізації

Спочатку вимірюй, потім оптимізуй

Головне правило оптимізації: не оптимізуй те, що не гальмує.

Спочатку профілюйте, знайдіть «гарячі точки», а вже потім змінюйте код. Іноді най«очевидніша» ділянка коду займає всього 1 % часу, а справжній «монстр» — десь у бібліотеці або в неочікуваному місці.

Використання профайлера для пошуку гарячих точок

Гаряча точка — це метод або ділянка коду, що займає найбільшу частку часу роботи застосунку.

У VisualVM це видно на вкладці CPU Profiler:

  • Сортуйте методи за часом виконання.
  • Дивіться на stack trace: хто кого викликає.
  • Пам’ятайте, що іноді «винен» не ваш код, а бібліотека або навіть JDK.

Приклади оптимізації

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

Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
    // швидко!
}

Приклад 2: Зменшення кількості виділень
Замість створення нових об’єктів у циклі використовуйте повторне використання або StringBuilder.

// Погано:
for (int i = 0; i < 10000; i++) {
    String s = "Результат: " + i;
}

// Краще:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.setLength(0);
    sb.append("Результат: ").append(i);
    String s = sb.toString();
}

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

Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
    return sqrtCache.computeIfAbsent(x, Math::sqrt);
}

6. Демонстрація: пришвидшуємо наш калькулятор

Проблема: simulateHeavyOperation() витрачає багато часу

Крок 1. Профілюємо
У VisualVM видно, що майже весь час іде на Math.sqrt(i) усередині циклу на 5_000_000 ітерацій.

Крок 2. Оптимізуємо
Якщо це просто симуляція навантаження — приберіть її або зменште кількість ітерацій.
Якщо це реальна бізнес-логіка — подумайте, чи можна:

  • Кешувати результат.
  • Використати швидший алгоритм.
  • Перенести обчислення в окремий потік (якщо це не критично для користувача).

Приклад оптимізації:

private void simulateHeavyOperation() {
    // Було 5_000_000, стало 100_000
    for (int i = 0; i < 100_000; i++) {
        Math.sqrt(i);
    }
}

Крок 3. Перевіряємо результат
Знову запускаємо профілювання — програма працює швидше, навантаження на CPU зменшилося.

7. Візуалізація: процес оптимізації

flowchart TD A[Запуск застосунку] B["Профілювання (VisualVM)"] C[Виявлення вузьких місць] D[Оптимізація коду] E[Повторне профілювання] F[Покращення продуктивності] A --> B --> C --> D --> E --> F E --> C

8. Типові помилки під час профілювання й оптимізації

Помилка № 1: Оптимізація «на око». Дуже часто розробники починають змінювати код, не вимірявши, де насправді проблема. У підсумку — багато роботи, а результат мінімальний.

Помилка № 2: Профілювання за «нереальних» умов. Профілювати треба на тих даних і з тим навантаженням, яке наближене до реального. Профілювання «на порожньому місці» може не виявити справжніх проблем.

Помилка № 3: Ігнорування витоків пам’яті. Якщо не дивитися на heap dump і не аналізувати посилання, можна довго не помічати, що програма «пухне» і скоро впаде.

Помилка № 4: Погоня за мікроскопічною оптимізацією. Не варто витрачати дні на прискорення коду, який займає 0.1 % часу роботи застосунку. Спочатку — головні вузькі місця.

Помилка № 5: Неврахування потоків і синхронізації. У багатопотокових застосунках проблеми з продуктивністю часто пов’язані не з алгоритмами, а з блокуваннями й очікуваннями (synchronized, contention).

Помилка № 6: Забули про профілювання після змін. Після оптимізації обов’язково перевірте результат: іноді «оптимізація» може навіть сповільнити роботу!

1
Опитування
Логування, рівень 63, лекція 4
Недоступний
Логування
Логування, моніторинг і профілювання
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ