В составе JDK есть полезная утилита jvisualvm (Java VisualVM), позволяющая снимать дампы, анализировать производительность, состояние потоков и памяти.
Для анализа создадим вот такую программу на Java «MainTestProgram.java»:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.Future; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; public class MainTestProgram { private static final int THREAD_COUNT = 10; private static final int TASKS_COUNT = 10; private static final long WORK_MINUTES = 60L; private static class MyClass{ private byte[] bytes = new byte[100_000]; } private static final ConcurrentMap<Integer, MyClass> concurrentMap = new ConcurrentHashMap<>(); private static class MyTask implements Callable<Boolean> { @Override public Boolean call() throws Exception { long startTime = System.currentTimeMillis(); // Каждый поток работает только WORK_MINUTES минут с момента старта. while (System.currentTimeMillis() < startTime + 1000L * 60L* WORK_MINUTES) { try { Thread.sleep(500L); } catch (InterruptedException ie) { ie.printStackTrace(); } concurrentMap.put( ThreadLocalRandom.current().nextInt( Integer.MIN_VALUE, Integer.MAX_VALUE), new MyClass()); } return true; } } public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT); Future<Boolean>[] futures = new Future[TASKS_COUNT]; System.out.println("Scheduling threads..."); for (int n = 0; n < TASKS_COUNT; n++) { futures[n] = executorService.submit(new MyTask()); } System.out.println("Awaiting termination..."); executorService.awaitTermination(WORK_MINUTES * 2L, TimeUnit.MINUTES); System.out.println("Finished."); } } |
Скомпилируем (в случае проблем рекомендую ознакомиться с вот этой статьей):
1 |
javac MainTestProgram.java |
И запустим на выполнение:
1 |
java MainTestProgram |
Теперь перейдём в каталог bin внутри JDK. У меня для Windows это было «C:\Program Files\Java\jdk1.8.0_91\bin», но у вас он может немного отличаться в зависимости от версии Java и операционной системы.
Запустим jvisualvm. В левой части окна Java VisualVM в дереве кликнем два раза на нашей программе MainTestProgram:
Должна появиться вкладка MainTestProgram (pid NNNN). Внутри неё будет ещё пять вкладок:
- Overview;
- Monitor;
- Threads;
- Sampler;
- Profiler;
На вкладке Overview отображается общая информация о версии Java-машины, системных параметрах и прочем.
На вкладке Monitor можно смотреть загрузку ЦП, выделение памяти, количество запущенных и активных потоков. Здесь можно заметить моменты работы сборщика мусора. Для нашей программы там будет отображено примерно такое:
Обратите внимание на правый верхний график. График Used heap постоянно растёт. Можно смотреть на него очень долго, но даже если будет происходить сборка мусора, то график всё равно будет продвигаться вверх. Это сигнализирует о возможной утечке памяти. В нашем случае это происходит в строках concurrentMap.put(, где мы постоянно создаём новые значения и кладём их в глобальный ConcurrentMap, в результате чего они всегда достижимы из кода и никогда не уничтожаются сборщиком мусора. В реальных приложениях найти причину утечки гораздо сложнее.
На вкладке Threads отображается состояние потоков. Для нашего случая это нечто такое:
Обратите внимание на потоки pool-1-thread-NN. Они все в состоянии Park, то есть чего-то ожидают. Когда все потоки из пулов находятся в таком состоянии, то это тоже плохой признак. Они могут ожидать ответа от другого сервера по сети или ещё чего-нибудь. В нашем случае они сидят в строке Thread.sleep(500L); нашего кода, что мы увидим в последующих вкладках Java VisualVM.
На вкладке Sampler мы можем замерять, количество созданных объектов каждого типа и количество затраченного времени на вызов каждого метода.
Для замера времени выполнения методов нужно кликнуть по кнопке CPU, после чего jvisualvm начнёт собирать данные. Можно делать snapshot-ы для сохранения в файл и последующего анализа:
Как вы можете увидеть, все потоки действительно висят на нашем Thread.sleep:
Теперь посмотрим на состояние памяти. Для этого снова вернитесь во вкладке Sampler и кликните на кнопку Memory, после чего jvisualvm начнёт собирать статистику использования памяти:
Информации, которую я уже изложил в статье, должно хватить для выявления большинства проблем и узких мест в вашем приложении. Осталось сказать ещё только одно: как подключиться к удалённому приложению на сервере?
Для подключения к приложению на удалённом сервере нужно сперва запустить на этом сервере утилиту jstatd, которая входит в состав JDK. Затем кликаем правой кнопкой мышки на узле Remote в jvisualvm и там на Add Remote Host…, после чего в появившемся окне необходимо указать имя узла и отображаемое имя. В остальном работа с удалённым приложением аналогична анализу локального приложения.
Сударь, просто отличная статья. Выражаю огромную благодарность, про утилитку не знал, невероятно полезна для сисадминов, как минимум.