Статьи

В куче против использования памяти кучи

обзор

Недавно меня спросили о преимуществах и мудрости использования памяти кучи в Java. Ответы могут представлять интерес для тех, кто сталкивается с тем же выбором.

От кучи памяти ничего особенного. Стеки потоков, код приложения, буферы NIO — все из кучи. На самом деле в C и C ++ у вас есть только неуправляемая память, поскольку у нее нет управляемой кучи по умолчанию. Использование управляемой памяти или «кучи» в Java — это особенность языка. Примечание: Java не единственный язык, который делает это.

новый объект () против пула объектов против выключенной памяти кучи

новый объект ()

До Java 5.0 использование объектных пулов было очень популярно. Создание объектов было еще очень дорого. Однако в Java 5.0 распределение объектов и очистка мусора стали намного дешевле, и разработчики обнаружили, что они получили ускорение производительности и упрощение своего кода, удаляя пулы объектов и просто создавая новые объекты при необходимости. До Java 5.0 почти любой пул объектов, даже пул объектов, который использовал объекты, обеспечил улучшение, по сравнению с Java 5.0, объединяющим в пул только дорогие объекты, очевидно, имело смысл, например, потоки, сокеты и соединения с базой данных.

Объектные бассейны

В пространстве с низкой задержкой все еще было очевидно, что утилизация изменяемых объектов повышает производительность за счет уменьшения нагрузки на кэш-память вашего процессора. Эти объекты должны иметь простые жизненные циклы и иметь простую структуру, но вы можете увидеть значительные улучшения в производительности и дрожании, используя их.

Другая область, в которой имеет смысл использовать пулы объектов, — это загрузка больших объемов данных со многими дублирующимися объектами. Со значительным сокращением использования памяти и уменьшением количества объектов, которыми должен управлять GC, вы увидели сокращение времени GC и увеличение пропускной способности.

Эти пулы объектов были разработаны, чтобы быть более легкими, чем, скажем, с использованием синхронизированного HashMap, и поэтому они все еще помогли.

Возьмите этот класс StringInterner в качестве примера. Вы передаете ему переработанный изменяемый StringBuilder текста, который вы хотите, как String, и он предоставляет String, который соответствует. Передача строки будет неэффективной, так как вы уже создали объект. StringBuilder может быть переработан.

Примечание: эта структура обладает интересным свойством, которое не требует никаких дополнительных функций безопасности потоков, таких как энергозависимая или синхронизированная, кроме тех, которые предусмотрены минимальными гарантиями Java. т.е. вы можете видеть последние поля в строке правильно и читать только согласованные ссылки.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StringInterner {
    private final String[] interner;
    private final int mask;
    public StringInterner(int capacity) {
        int n = Maths.nextPower2(capacity, 128);
        interner = new String[n];
        mask = n - 1;
    }
 
    private static boolean isEqual(@Nullable CharSequence s, @NotNull CharSequence cs) {
        if (s == null) return false;
        if (s.length() != cs.length()) return false;
        for (int i = 0; i < cs.length(); i++)
            if (s.charAt(i) != cs.charAt(i))
                return false;
        return true;
    }
 
    @NotNull
    public String intern(@NotNull CharSequence cs) {
        long hash = 0;
        for (int i = 0; i < cs.length(); i++)
            hash = 57 * hash + cs.charAt(i);
        int h = (int) Maths.hash(hash) & mask;
        String s = interner[h];
        if (isEqual(s, cs))
            return s;
        String s2 = cs.toString();
        return interner[h] = s2;
    }
}

Использование памяти вне кучи

Использование памяти вне кучи и использование пулов объектов помогают сократить паузы GC, это их единственное сходство. Пулы объектов хороши для непродолжительных изменяемых объектов, дороги для создания объектов и долгоживущих неизменяемых объектов, где много дублирования. Изменяемые объекты со средним сроком службы или сложные объекты с большей вероятностью лучше оставить для обработки в GC. Тем не менее, изменяемые объекты средней и долгой жизни страдают несколькими способами, которые решает нехватка памяти.

Вне кучи памяти обеспечивает;

  • Масштабируемость до больших объемов памяти, например, более 1 ТБ и больше, чем основная память.
  • Условное влияние на время паузы ГХ.
  • Совместное использование процессов, уменьшение дублирования между JVM и упрощение разделения JVM.
  • Постоянство для более быстрого перезапуска или ответа на производственные данные в тесте.

Использование памяти вне кучи дает вам больше возможностей с точки зрения дизайна вашей системы. Самым важным улучшением является не производительность, а детерминизм.

От кучи и тестирования

Одной из самых больших проблем в высокопроизводительных вычислениях является воспроизведение неясных ошибок и возможность доказать, что вы их исправили. Постоянно сохраняя все входные события и данные из кучи, вы можете превратить критически важные системы в ряд сложных конечных автоматов. (Или в простых случаях, только один конечный автомат). Таким образом, вы получаете воспроизводимое поведение и производительность между тестированием и производством.

Многие инвестиционные банки используют эту технику, чтобы надежно воспроизвести систему для любого события в течение дня и точно определить, почему это событие было обработано так, как оно было. Что еще более важно, если у вас есть исправление, вы можете показать, что исправили проблему, возникшую в производстве, вместо того, чтобы найти проблему и надеяться, что это проблема.

Наряду с детерминированным поведением приходит детерминистская производительность. В тестовых средах вы можете воспроизводить события с реалистичной синхронизацией и отображать распределение задержки, которое вы ожидаете получить в работе. Некоторый системный джиттер не может быть воспроизведен, особенно если аппаратное обеспечение не одинаково, но вы можете подойти довольно близко, когда вы используете статистическое представление. Чтобы не использовать день для воспроизведения дня данных, вы можете добавить порог. Например, если время между событиями превышает 10 мс, вы можете подождать только 10 мс. Это может позволить вам воспроизвести день событий с реалистичной синхронизацией менее чем за час и посмотреть, улучшили ли ваши изменения распределение задержек или нет.

Переходя на более низкий уровень, не теряете ли вы часть «скомпилируйте один раз, бегите куда угодно»?

В некоторой степени это правда, но это гораздо меньше, чем вы думаете. Когда вы работаете ближе с процессором, вы больше зависите от того, как ведет себя процессор или ОС. К счастью, большинство систем используют процессоры AMD / Intel, и даже процессоры ARM становятся более совместимыми с точки зрения гарантий низкого уровня, которые они предоставляют. Существуют также различия в операционных системах, и эти методы работают лучше в Linux, чем в Windows. Однако, если вы разрабатываете на MacOSX или Windows и используете Linux для производства, у вас не должно возникнуть никаких проблем. Это то, что мы делаем в торговле на высоких частотах.

Какие новые проблемы мы создаем, используя вне кучи?

Ничто не приходит бесплатно, и это в случае с кучей. Самая большая проблема с кучей вне системы — ваши структуры данных становятся менее естественными. Вам либо нужна простая структура данных, которая может быть отображена непосредственно в кучу, либо у вас есть сложная структура данных, которая сериализуется и десериализуется, чтобы удалить ее из кучи. Очевидное использование сериализации имеет свои головные боли и снижение производительности. Таким образом, использование сериализации намного медленнее, чем для объектов кучи.

В финансовом мире структура данных с самым высоким тиковым числом является плоской и простой, полной примитивов, которые красиво отображаются в куче с небольшими издержками. Однако это применимо не ко всем приложениям, и вы можете получить сложные вложенные структуры данных, например, графики, которые могут в конечном итоге также привести к необходимости кэшировать некоторые объекты в куче.

Другая проблема заключается в том, что JVM ограничивает объем системы, которую вы можете использовать. Вам не нужно беспокоиться о том, что JVM перегружает систему. Вне кучи некоторые ограничения сняты, и вы можете использовать структуры данных, которые намного больше, чем основная память, и вы начинаете беспокоиться о том, какая у вас дисковая подсистема, если вы делаете это. Например, вы не хотите осуществлять пейджинг на жесткий диск с 80 IOPS, вместо этого вы, вероятно, захотите SSD с 80 000 IOPS (операций ввода / вывода в секунду) или лучше, то есть в 1000 раз быстрее.

Как OpenHFT помогает?

OpenHFT имеет ряд библиотек, которые скрывают тот факт, что вы действительно используете встроенную память для хранения ваших данных. Эти структуры данных сохраняются и могут использоваться практически без мусора. Они используются в приложениях, которые работают весь день без незначительного сбора

Chronicle Queue — Постоянная очередь событий. Поддерживает одновременные записи на JVM на одной машине и одновременные чтения на разных машинах. Задержки в микросекунды и постоянная пропускная способность в миллионах сообщений в секунду.

Хроническая карта — собственное или постоянное хранилище карты ключ-значение. Может использоваться совместно JVM на одном компьютере, реплицироваться через UDP или TCP и / или удаленно получать доступ через TCP. Задержки в микросекунды и постоянные скорости чтения / записи в миллионах операций в секунду на машину.

Thread Affinity — привязка критических потоков к изолированным ядрам или логическим процессорам для минимизации джиттера. Может уменьшить джиттер в 1000 раз.

Какой API использовать?

Если вам нужно записать каждое событие -> Chronicle Queue

Если вам нужен только последний результат для уникального ключа -> Карта хроники

Если вас волнует джиттер 20 микросекунд -> Thread Affinity

Вывод

Память без кучи может иметь проблемы, но также может принести много преимуществ. Где вы видите наибольший выигрыш и сравнение с другими решениями, представленными для достижения масштабируемости. Отключение кучи, вероятно, будет проще и намного быстрее, чем использование многораздельных / защищенных кэшей кучи, решений для обмена сообщениями или внепроцессных баз данных. Быстрее, вы можете обнаружить, что некоторые из приемов, которые вам нужно сделать, чтобы дать вам производительность, вам больше не нужны. Например, решения без кучи могут поддерживать синхронную запись в ОС, вместо того чтобы выполнять их асинхронно с риском потери данных.

Самым большим выигрышем, однако, может быть время запуска, обеспечивающее производственную систему, которая перезапускается намного быстрее. Например, отображение в наборе данных объемом 1 ТБ может занять 10 миллисекунд, и простота воспроизведения в тесте путем воспроизведения каждого события, чтобы вы каждый раз получали одинаковое поведение. Это позволяет вам производить качественные системы, на которые вы можете положиться.

Ссылка: От кучи против использования кучи памяти от нашего партнера JCG Питера Лоури из блога Vanilla Java .