Райское пространство
Поэтому мой вопрос заключается в том, может ли все это действительно быть правдой, и если да, то почему распределение кучи в java происходит намного быстрее.
Я немного изучил, как работает Java GC, так как он мне очень интересен. Я всегда пытаюсь расширить свою коллекцию стратегий выделения памяти в C и C ++ (интересуюсь попыткой реализовать что-то подобное в C), и это очень, очень быстрый способ распределить множество объектов в пакетном режиме из практическая перспектива, но в первую очередь из-за многопоточности.
Способ распределения Java GC работает с использованием чрезвычайно дешевой стратегии выделения для первоначального размещения объектов в пространстве «Eden». Насколько я могу судить, он использует последовательный распределитель пулов.
Это намного быстрее с точки зрения алгоритма и уменьшения количества обязательных отказов страниц, чем общего назначения malloc
в C или по умолчанию, operator new
в C ++.
Но у последовательных распределителей есть явный недостаток: они могут выделять куски переменного размера, но они не могут освободить отдельные куски. Они просто распределяются прямым последовательным образом с отступами для выравнивания и могут очищать только всю выделенную память одновременно. Они обычно полезны в C и C ++ для построения структур данных, которые требуют только вставки и не удаляют элементы, например, дерево поиска, которое нужно построить только один раз при запуске программы, а затем повторно искать или добавлять только новые ключи ( ключи не удалены).
Они также могут использоваться даже для структур данных, которые позволяют удалять элементы, но эти элементы фактически не будут освобождены из памяти, поскольку мы не можем освободить их по отдельности. Такая структура, использующая последовательный распределитель, просто потребляла бы все больше и больше памяти, если у нее не было некоторого отложенного прохода, когда данные копировались в свежую, сжатую копию с использованием отдельного последовательного распределителя (и иногда это очень эффективный метод, если победил фиксированный распределитель). по какой-то причине - просто последовательно выделите новую копию структуры данных и сбросьте всю память старой).
Коллекция
Как и в приведенном выше примере структуры данных / последовательного пула, было бы огромной проблемой, если бы Java GC выделял только этот путь, даже если он очень быстр для пакетного распределения многих отдельных блоков. Он не сможет ничего освободить, пока программное обеспечение не будет закрыто, и в этот момент оно сможет освободить (очистить) все пулы памяти одновременно.
Таким образом, вместо этого, после одного цикла GC, выполняется проход через существующие объекты в пространстве «Eden» (последовательно выделяемые), и те, на которые все еще ссылаются, затем выделяются с использованием распределителя более общего назначения, способного освобождать отдельные фрагменты. Те, на кого больше нет ссылок, будут просто освобождены в процессе очистки. Так что в основном это «копирование объектов из пространства Eden, если на них все еще есть ссылки, а затем очистка».
Обычно это было бы довольно дорого, поэтому это делается в отдельном фоновом потоке, чтобы избежать существенного останова потока, который первоначально выделил всю память.
Как только память скопирована из пространства Eden и выделена с использованием этой более дорогой схемы, которая может освободить отдельные фрагменты после начального цикла GC, объекты перемещаются в более постоянную область памяти. Эти отдельные куски затем освобождаются в последующих циклах GC, если они перестают ссылаться.
скорость
Таким образом, грубо говоря, причина, по которой Java GC может значительно превзойти C или C ++ при прямом выделении кучи, заключается в том, что он использует самую дешевую, полностью вырожденную стратегию выделения в потоке, запрашивающем выделение памяти. Тогда это экономит более дорогую работу, которую мы обычно должны выполнять при использовании более общего распределителя, такого как прямой malloc
поток для другого потока.
Таким образом, концептуально GC фактически должен выполнять в целом больше работы, но распределяет ее по потокам, чтобы полная стоимость не оплачивалась одним потоком. Это позволяет потоку, выделяющему память, делать это очень дешево, а затем откладывать истинные затраты, необходимые для правильного выполнения работы, чтобы отдельные объекты могли быть фактически освобождены в другой поток. В C или C ++, когда мы malloc
или звоним operator new
, мы должны оплатить полную стоимость авансом в пределах одного потока.
В этом главное отличие, и именно поэтому Java может очень выиграть у C или C ++, используя просто наивные вызовы malloc
или operator new
выделять кучу маленьких кусочков по отдельности. Конечно, когда цикл GC запускается, как правило, будут некоторые атомарные операции и некоторая потенциальная блокировка, но, вероятно, он немного оптимизирован.
По сути, простое объяснение сводится к тому, чтобы платить более высокую стоимость в одном потоке ( malloc
), а не более дешевую стоимость в одном потоке, а затем платить более высокую стоимость в другом, который может работать параллельно ( GC
). В качестве недостатка при выполнении таких действий подразумевается, что вам необходимо получить две косвенные ссылки для получения ссылки на объект на объект, как это требуется, чтобы распределитель мог копировать / перемещать память без аннулирования существующих ссылок на объекты, а также вы можете потерять пространственную локальность, как только объектная память станет вышел из "райского" пространства.
И последнее, но не менее важное: сравнение немного несправедливо, поскольку код C ++ обычно не выделяет кучу объектов по отдельности в куче. Достойный код C ++ имеет тенденцию выделять память для многих элементов в смежных блоках или в стеке. Если он распределяет кучу крошечных объектов по одному в бесплатном магазине, код будет дерьмовым.