Каковы «лучшие практики» для создания (и выпуска) миллионов небольших объектов?
Я пишу шахматную программу на Java, и алгоритм поиска генерирует один объект «Ход» для каждого возможного хода, а номинальный поиск может легко генерировать более миллиона объектов ходов в секунду. Сборщик мусора JVM смог справиться с нагрузкой на мою систему разработки, но мне интересно изучить альтернативные подходы, которые:
- Минимизируйте накладные расходы на сборку мусора и
- уменьшить пиковую нагрузку на память для систем начального уровня.
Подавляющее большинство объектов очень недолговечны, но около 1% сгенерированных перемещений сохраняется и возвращается как постоянное значение, поэтому любой метод объединения или кэширования должен обеспечивать возможность исключения определенных объектов из повторного использования. .
Я не ожидаю полностью детализированного примера кода, но я был бы признателен за предложения для дальнейшего чтения / исследования или примеры с открытым исходным кодом аналогичного характера.
источник
Ответы:
Запустите приложение с подробной сборкой мусора:
И он скажет вам, когда он соберется. Будет два типа разверток: быстрая и полная.
Стрелка до и после размера.
Пока он просто выполняет сборку мусора, а не полную сборку мусора, вы дома в безопасности. Обычный сборщик мусора является сборщиком копий в «молодом поколении», поэтому объекты, на которые больше не ссылаются, просто забываются, а это именно то, что вам нужно.
Чтение настройки сборки мусора виртуальной машины Java SE 6 HotSpot, вероятно, будет полезным.
источник
Начиная с версии 6, в серверном режиме JVM используется метод анализа выхода . Используя его, вы можете полностью избежать сборки мусора.
источник
Что ж, тут несколько вопросов в одном!
1 - Как управляются недолговечные объекты?
Как указывалось ранее, JVM может отлично справиться с огромным количеством короткоживущих объектов, поскольку следует гипотезе слабого поколения .
Обратите внимание, что мы говорим об объектах, которые достигли основной памяти (кучи). Это не всегда так. Многие создаваемые вами объекты даже не покидают регистр ЦП. Например, рассмотрим этот цикл for
Давайте не будем думать о развертывании цикла (оптимизации, которую JVM серьезно выполняет в вашем коде). Если
max
равноInteger.MAX_VALUE
, выполнение цикла может занять некоторое время. Однакоi
переменная никогда не выйдет из блока цикла. Поэтому JVM поместит эту переменную в регистр ЦП, регулярно будет увеличивать ее, но никогда не будет отправлять ее обратно в основную память.Итак, создание миллионов объектов не представляет большого труда, если они используются только локально. Они будут мертвы до того, как будут сохранены в Эдеме, поэтому ГК их даже не заметит.
2 - Полезно ли уменьшить накладные расходы на сборщик мусора?
Как обычно, это зависит от обстоятельств.
Во-первых, вы должны включить ведение журнала GC, чтобы иметь четкое представление о том, что происходит. Вы можете включить его с помощью
-Xloggc:gc.log -XX:+PrintGCDetails
.Если ваше приложение проводит много времени в цикле сборщика мусора, то да, настройте сборщик мусора, в противном случае оно того не стоит.
Например, если у вас есть молодой сборщик мусора каждые 100 мс, который занимает 10 мс, вы проводите 10% своего времени в сборщике мусора, и у вас есть 10 сборов в секунду (что очень много). В таком случае я бы не стал тратить время на настройку сборщика мусора, поскольку эти 10 сборщиков мусора в секунду все равно будут там.
3 - Некоторый опыт
У меня была аналогичная проблема с приложением, которое создавало огромное количество заданного класса. В журналах GC я заметил, что скорость создания приложения составляла около 3 ГБ / с, что слишком много (да ладно ... 3 гигабайта данных каждую секунду ?!).
Проблема: слишком много частых сборщиков мусора из-за создания слишком большого количества объектов.
В моем случае я подключил профилировщик памяти и заметил, что класс представляет собой огромный процент всех моих объектов. Я отследил экземпляры, чтобы выяснить, что этот класс в основном представляет собой пару логических значений, заключенных в объект. В этом случае было доступно два решения:
Измените алгоритм, чтобы я не возвращал пару логических значений, а вместо этого у меня было два метода, которые возвращают каждое логическое значение отдельно.
Кешируйте объекты, зная, что было всего 4 разных экземпляра
Я выбрал второй, так как он меньше всего повлиял на приложение и его было легко внедрить. Мне потребовалось несколько минут, чтобы создать фабрику с небезопасным кешем (потокобезопасность мне не нужна, поскольку в конечном итоге у меня будет только 4 разных экземпляра).
Скорость выделения снизилась до 1 ГБ / с, как и частота молодых GC (деленная на 3).
Надеюсь, это поможет !
источник
Если у вас есть только объекты-значения (то есть нет ссылок на другие объекты) и на самом деле, но я имею в виду действительно тонны и тонны из них, вы можете использовать прямое
ByteBuffers
с собственным порядком байтов (последнее важно), и вам нужно несколько сотен строк код для распределения / повторного использования + геттеров / сеттеров. Геттеры похожи наlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}
Это решит проблему GC почти полностью, если вы выделяете только один раз, то есть огромный кусок, а затем сами управляете объектами. Вместо ссылок у вас будет только индекс (то есть
int
) в то,ByteBuffer
что нужно передать. Возможно, вам также потребуется выполнить выравнивание памяти самостоятельно.Технику хотелось бы использовать
C and void*
, но с некоторым укутыванием это терпимо. Обратной стороной производительности может быть проверка границ, если компилятор не может ее устранить. Основным преимуществом является локальность, если вы обрабатываете кортежи как векторы, отсутствие заголовка объекта также уменьшает объем памяти.В остальном такой подход, скорее всего, вам не понадобится, поскольку молодое поколение практически всех JVM тривиально умирает, а затраты на выделение - всего лишь скачок указателя. Стоимость распределения может быть немного выше, если вы используете
final
поля, так как они требуют ограничения памяти на некоторых платформах (а именно ARM / Power), хотя на x86 это бесплатно.источник
Предполагая, что вы обнаружите, что сборщик мусора является проблемой (как указывают другие, это может быть не так), вы будете реализовывать свое собственное управление памятью для своего особого случая, то есть класса, который страдает от массового оттока. Попробуйте объединение объектов, я видел случаи, когда он работал достаточно хорошо. Реализация пулов объектов - это хорошо проторенный путь, поэтому не нужно возвращаться сюда повторно, обратите внимание на:
Измерьте до / после и т. Д. И т. Д.
источник
Встречал похожую проблему. Прежде всего, постарайтесь уменьшить размер мелких предметов. Мы ввели некоторые значения полей по умолчанию, ссылающиеся на них в каждом экземпляре объекта.
Например, MouseEvent имеет ссылку на класс Point. Мы кэшировали точки и ссылались на них вместо создания новых экземпляров. То же самое, например, для пустых строк.
Другим источником было несколько логических значений, которые были заменены одним int, и для каждого логического значения мы использовали только один байт int.
источник
Некоторое время назад я имел дело с этим сценарием с некоторым кодом обработки XML. Я обнаружил, что создаю миллионы объектов тегов XML, которые были очень маленькими (обычно просто строка) и чрезвычайно недолговечными (неудача проверки XPath означала отсутствие совпадений, поэтому откажитесь).
Я провел серьезное тестирование и пришел к выводу, что могу добиться увеличения скорости только примерно на 7%, используя список отброшенных тегов вместо создания новых. Однако после реализации я обнаружил, что для свободной очереди нужен механизм, который будет ее сокращать, если она станет слишком большой - это полностью свело на нет мою оптимизацию, поэтому я переключил ее на опцию.
В общем - вероятно, того не стоит - но я рад видеть, что вы думаете об этом, это показывает вашу заботу.
источник
Учитывая, что вы пишете шахматную программу, есть несколько специальных приемов, которые можно использовать для приличной производительности. Один из простых подходов - создать большой массив длинных чисел (или байтов) и рассматривать его как стек. Каждый раз, когда ваш генератор ходов создает ходы, он помещает в стек пару чисел, например, переход от квадрата к квадрату. По мере того, как вы оцениваете дерево поиска, вы будете показывать ходы и обновлять изображение на доске.
Если вы хотите выразительной силы, используйте предметы. Если вам нужна скорость (в данном случае), переходите на родную.
источник
Одно из решений, которое я использовал для таких алгоритмов поиска, - это создать только один объект Move, изменить его новым перемещением, а затем отменить перемещение перед выходом из области видимости. Вы, вероятно, анализируете только один ход за раз, а затем просто где-то сохраняете лучший ход.
Если по какой-то причине это невозможно, и вы хотите уменьшить пиковое использование памяти, хорошая статья об эффективности памяти находится здесь: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf
источник
Просто создайте свои миллионы объектов и напишите свой код надлежащим образом: не храните ненужные ссылки на эти объекты. GC сделает за вас грязную работу. Вы можете поэкспериментировать с подробным сборщиком мусора, как уже упоминалось, чтобы увидеть, действительно ли они GC'd. Java IS о создании и выпуске объектов. :)
источник
Я думаю, вам стоит прочитать о распределении стека в Java и анализе выхода.
Потому что, если вы углубитесь в эту тему, вы можете обнаружить, что ваши объекты даже не выделены в куче, и они не собираются GC, как объекты в куче.
В Википедии есть объяснение анализа побега с примером того, как это работает в Java:
http://en.wikipedia.org/wiki/Escape_analysis
источник
Я не большой поклонник GC, поэтому всегда пытаюсь найти способы обойти его. В этом случае я бы предложил использовать шаблон Object Pool :
Идея состоит в том, чтобы избежать создания новых объектов, сохранив их в стеке, чтобы вы могли повторно использовать его позже.
источник
Пулы объектов обеспечивают огромные (иногда в 10 раз) улучшения по сравнению с распределением объектов в куче. Но приведенная выше реализация с использованием связанного списка наивна и неверна! Связанный список создает объекты для управления своей внутренней структурой, сводя на нет усилия. Кольцевой буфер, использующий массив объектов, работает хорошо. В примере give (шахматная программа, управляющая ходами) кольцевой буфер должен быть заключен в объект-держатель для списка всех вычисленных ходов. Затем будут передаваться только ссылки на объекты-держатели ходов.
источник