Введение: зачем профилировать Java-код?
Разработка на Java — это не только написание корректного кода, но и создание быстрых, масштабируемых приложений. Даже идеально спроектированная система может тормозить из-за одной неоптимальной строки. Как найти эту «бутылочное горлышко»? Ответ — профилирование.
Профилирование — это процесс сбора данных о работе приложения: времени выполнения методов, использовании памяти, потоках и загрузке CPU. Без него оптимизация превращается в гадание. В этой статье мы разберем практические примеры профилирования Java-кода, научимся интерпретировать результаты и находить узкие места.
Мы рассмотрим два популярных инструмента: VisualVM (бесплатный, встроенный в JDK) и JProfiler (коммерческий, но мощный). Все примеры будут с реальным кодом.
Основная часть: практика профилирования
1. Настройка окружения и запуск профилировщика
Для начала убедитесь, что у вас установлен JDK (8 или выше). VisualVM поставляется вместе с JDK 8 и 9, для более новых версий его нужно скачать отдельно с официального сайта.
Напишем простой «тормозной» код для теста. Создайте класс SlowApp.java:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class SlowApp {
public static void main(String[] args) throws InterruptedException {
List<String> data = new ArrayList<>();
Random random = new Random();
// Симуляция долгой загрузки данных
for (int i = 0; i < 100_000; i++) {
data.add(generateString(random));
}
// Обработка данных
long sum = 0;
for (String s : data) {
sum += processString(s);
}
System.out.println("Result: " + sum);
Thread.sleep(10000); // Чтобы успеть подключиться профайлером
}
private static String generateString(Random random) {
StringBuilder sb = new StringBuilder(100);
for (int i = 0; i < 100; i++) {
sb.append((char) ('a' + random.nextInt(26)));
}
return sb.toString();
}
private static long processString(String s) {
long result = 0;
for (char c : s.toCharArray()) {
result += c;
// Искусственная задержка
for (int j = 0; j < 100; j++) {
result += Math.sqrt(j);
}
}
return result;
}
}
Запустите приложение:
javac SlowApp.java
java SlowApp
Пока программа работает (она заснет на 10 секунд), откройте VisualVM. В левой панели найдите ваш процесс (SlowApp) и дважды кликните. Перейдите на вкладку Sampler или Profiler — вы увидите статистику по CPU и памяти.
2. Анализ CPU: находим самый тяжелый метод
В VisualVM нажмите кнопку CPU в разделе Sampler. Через несколько секунд вы получите таблицу с методами, отсортированными по времени выполнения (Self Time).
В нашем примере вы увидите, что метод processString занимает более 80% времени. Внутри него — вложенный цикл Math.sqrt(j). Это и есть узкое место.
Как это интерпретировать?
- Self Time — время, потраченное непосредственно на выполнение метода (без учета вызовов дочерних методов).
- Total Time — общее время, включая все вложенные вызовы.
- Invocations — количество вызовов метода.
Пример вывода (упрощенно):
Method Self Time (ms) Invocations
SlowApp.processString 8500 100000
SlowApp.generateString 1200 100000
java.lang.Math.sqrt 6000 10000000
Очевидно, что Math.sqrt вызывается 10 миллионов раз (100000 строк * 100 итераций). Это и есть кандидат на оптимизацию. В реальном проекте таким «тяжелым» методом может быть работа с БД, парсинг JSON или сложные вычисления.
Практический совет: Если метод вызывается миллионы раз, даже микрооптимизация даст прирост. Попробуйте заменить Math.sqrt на предвычисленные значения или кэширование.
3. Профилирование памяти: утечки и сборка мусора
Вторая по важности метрика — память. Переключитесь на вкладку Monitor в VisualVM. Вы увидите графики Heap, Metaspace и GC activity.
Добавим в наш пример классическую утечку памяти (закомментируйте предыдущий код и используйте этот):
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class MemoryLeak {
public static Map<String, byte[]> cache = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
Random random = new Random();
while (true) {
String key = "key" + random.nextInt(1000);
// Утечка: каждый раз создаем новый массив, но не удаляем старые
cache.put(key, new byte[1024 * 100]); // 100 KB
Thread.sleep(10);
if (cache.size() > 10000) {
System.out.println("Cache size: " + cache.size());
}
}
}
}
Запустите и откройте в VisualVM. График Heap будет неуклонно расти, пока не упрется в максимум. После этого начнутся частые Full GC (сборки мусора), что приведет к паузам (Stop-The-World).
Чтобы найти источник утечки:
- Перейдите на вкладку Sampler → Memory.
- Нажмите Heap Dump (снимок кучи).
- Откроется окно с объектами. Отсортируйте по размеру (Retained Size).
- Вы увидите, что
byte[]иHashMap$Nodeзанимают больше всего памяти.
Двойной клик по классу покажет, откуда они создаются. В нашем случае — это метод main. В реальности утечки часто происходят из-за статических коллекций, слушателей событий или неправильного закрытия ресурсов.
4. Продвинутые техники: JProfiler и Flame Graph
VisualVM хорош для быстрого