Комментарии 10
Неплохая статья для систематизации знаний, спасибо!
Спасибо автору! Интересная и полезная обзорная статья. Почерпнул для себя пару практических моментов, на которые раньше не обращал внимание
Спасибо. Полезный и дельный лонгрид.
Но не могу устоять от пары замечаний.
Java SE - "больше всего подходящие для работы приложений для десктопа". Это, конечно, неверно.
(Да некоторая часть SDK посвящена поддержке разработки графических интерфейсов, но не более того)
И более того, сейчас множество серверного софта пишется именно в рамках Java SE. И уровня предприятия в том числе.
P.S. и, к слову, поддержка баз данных aka JDBC это часть Java SE, а не JEE.Но так же не верно утверждать что никто не пишет десктопные приложения на Java.
Собственно весь спектр десктопных приложений для разработки на Java(и не только), на Java SE и написан.
Все эти Eclipse, IntelliJ IDEA, SmartGit, DBeaver, EditiX и т.д. и т.п. - сотни их, на самом деле.Serial GC
Многопоточность не бесплатна. Переключение между потоками - очень дорогая операция для CPU.
Поэтому, если для JVM доступно очень мало логических процессорных ядер(<=2),то все GC активно использующие дополнительные потоки(а это все кроме Serial GC)
ничего не дают. В таких условиях, они скорее всего будут работать хуже чем Serial GC. А единственное доступное для JVM ядро - это не такая уж экзотическая история.
В мире полно одноядерных платформ где JVM вполне применяют. Да и в контейнерах (Docker and co.) такое бывает.
Поэтому Serial GC живее всех живых не смотря на прямолинейность.Parallel GC Тут стоило бы сказать что этот GC очень часто лучший выбор для "короткоживущих" процессов. Запустился - отработал - завершился. Например, java приложения запускаемые по СRON. Например, сборки проектов с gradle/maven.
В общем, сценарий очень не редкий и throughput GC, в таких сценариях, может дать знатнейший выигрыш (вплоть до 10-ков процентов).
Поэтому он нужен и важен.
Спасибо за детальный комментарий!
1 и 2. К сожалению, я был неправильно понят из-за некорректно подобранной формулировки.
> Java SE - "больше всего подходящие для работы приложений для десктопа".
Это, конечно, неверно.
Если это понимать как "Java SE — только для десктопа", то конечно нет. Имелось в виду "Если у вас десктоп, то вы хотите иметь именно Java SE, а не Java EE, например, или Java ME". Думаю тут спорить не с чем.
> P.S. и, к слову, поддержка баз данных aka JDBC это часть Java SE, а не JEE.
В статье напрямую про JDBC не сказано, скорее имелось в виду JPA, но соглашусь, что в формулировке "возможность взаимодействия с СУБД," наверное многие подумают про JDBC.
3.
> Поэтому, если для JVM доступно очень мало логических процессорных ядер(<=2),то все GC активно использующие дополнительные потоки(а это все кроме Serial GC) ничего не дают. В таких условиях, они скорее всего будут работать хуже чем Serial GC. А единственное доступное для JVM ядро - это не такая уж экзотическая история.
Если у вас N ядер, то по умолчанию ParallelGC не будет делать больше N потоков, то есть на одном CPU будет один тред и в ParallelGC, если пользователь не выстрелил себе в ногу и не выставил большое явное значение через
-XX:ParallelGCThreads
и проблем с излишним их переключением (по крайней мере, если не рассматривать приложения вне самой JVM) нет:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html
> On a machine with N hardware threads where N is greater than 8, the parallel collector uses a fixed fraction of N as the number of garbage collector threads. The fraction is approximately 5/8 for large values of N. At values of N below 8, the number used is N
Причем ограничения контейнеров тоже учитываются, и если контейнеру выделено, допустим, 1 ядро, а JVM видит на машине 8, то будет только один поток ParallelGC:
https://blogs.oracle.com/java/post/java-se-support-for-docker-cpu-and-memory-limits.
Опять же, это можно переопределить через
-XX:ActiveProcessorCount
4. Я бы сказал, ParallelGC хорош для gradle/maven и других короткоживущих процессов не потому, что они короткоживущие (сборка большого проекта может идти десятки минут), а потому что они, как правило, неинтерактивные. Latency в них нигде себя не проявит, а thoughput будет влиять на время выполнения.
Насколько я понимаю, ParallelGC это всегда дополнительный поток, пусть и только 1. т.е. у вас есть поток с приложением и поток(и) в котором работает этот ParallelGC. Если у вас в распоряжение есть ровно одно ядро CPU, то у вас переключение по крайне мере между этими двумя потоками. И это, по-видимому, убивает смысл всей затеи.
Иначе, JVM, по умолчанию, в этой ситуации использовала бы Parallel, а не Serial GC, думается мне.
Вообще да, так и есть, но есть нюанс.
Написал самое простое приложение, которое просто бесконечно спит:
import java.lang.Thread;
class Sleep {
public static void main(String [] argv) throws InterruptedException {
while (true) {
Thread.sleep(Long.MAX_VALUE);
}
}
}
При его запуске у меня на Java SE 11 появляется 12 потоков с ParallelGC и 11 c Serial. То есть во время работы приложения работает JIT-компилятор, обработка сигналов, и так далее — все в разных потоках. Так что согласен, разница есть, но разница не между одним-двумя, а между 11-12 потоками — вряд ли она сильно влияет в такой ситуации. Нужна специальная оптимизация, уже за пределами выбора GC, чтобы от них избавиться.
Очень интересно, да.
Однако.
Полагаю, все эти потоки, кроме основного потока приложения и потока GC, после начального этапа старта приложения (когда все компилируется/оптимизируется), находятся практически всегда в состоянии сна.
И за единственный доступный CPU будут постоянно бороться только поток GC и основной. И мы, в теории, приходим к той же ситуации, как если бы их было ровно два. C тем же эффектом.
Но очень не просто все это проверить.
Однозначно будут особенности и от JVM-рантайма (которых много есть, не только HotSpot), и от платформы-железа (Бог его ведает как оно работает скажем на Risk V)
Я опять же склонен доверять дефолту от разработчиков JVM. Раз они считают что Serial GC более оптимален в такой ситуации, то скорее всего так оно и есть.
Бороться с самим приложением поток GC не будет, все-таки оба GC требуют stop-the-world на протяжении всей своей работы. Но да, минимум одно переключение потока будет, в случае c Parallel GC.
Но я бы все равно на этом не зацикливался.
Есть такая штука: https://gist.github.com/jboner/2841832, в ней примерно (с точностью до порядка) на 2012 год описано время выполнения тех или иных операций. Согласно ей, только прочесть мегабайт данных из памяти — примерно 250 микросекунд.
А вот еще статья https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html, там множество разных бенчмарков, но даже в самом худшем время на context switch оценивается в 4300 наносекунд, т.е 4.3 микросекунды.
Так что если у вас heap измеряется не килобайтами, то задержка из-за смены контекста не должна превысить пару процентов.
Добавлю интересную информацию о 32+Gb Heap.
По работе имел дело с распределенным in-memory кешем который был Java приложением. И для того чтобы увеличить его размер, помня про compressedOOPs, установили хип в 32Gb и не смогли кэш загрузить, падал с OOM. После долгих плясок с бубном выяснилось, что кап в 32Gb на самом деле не 32Gb и найти информацию об этом почти нереально. А корень проблемы в том, что начиная с Java 8 PermGen не входит в heap. Т.е. установив Xmx=32g, мы реально имеем 32+PermGen size, а т.к. это явно больше чем 32 то Java переходит на 64bit pointers.
Java для сисадминов