Сколько памяти занимает строка java

Обновлено: 05.07.2024

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

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

Примечание. Исходный код этой статьи можно загрузить из ресурсов.

Инструмент

Поскольку Java намеренно скрывает многие аспекты управления памятью, определение того, сколько памяти потребляют ваши объекты, требует определенных усилий. Вы можете использовать метод Runtime.freeMemory() для измерения различий в размерах кучи до и после выделения нескольких объектов. В нескольких статьях, таких как «Вопрос недели № 107» Рамчандера Варадараджана (Sun Microsystems, сентябрь 2000 г.) и «Память имеет значение» Тони Синтеса (JavaWorld, декабрь 2001 г.), подробно излагается эта идея. К сожалению, решение из первой статьи дает сбой, потому что в реализации используется неверный метод среды выполнения, а решение из последней статьи имеет свои недостатки:

  • Одного вызова Runtime.freeMemory() оказывается недостаточно, поскольку JVM может принять решение об увеличении текущего размера кучи в любое время (особенно при сборке мусора). Если общий размер кучи уже не равен максимальному размеру -Xmx, мы должны использовать Runtime.totalMemory()-Runtime.freeMemory() в качестве используемого размера кучи.
  • Выполнение одного вызова Runtime.gc() может оказаться недостаточно агрессивным для запроса сборки мусора. Мы могли бы, например, запросить запуск финализаторов объектов. А поскольку Runtime.gc() не задокументировано для блокировки до завершения сбора, рекомендуется подождать, пока предполагаемый размер кучи не стабилизируется.
  • Если профилируемый класс создает какие-либо статические данные как часть инициализации своего класса для каждого класса (включая инициализаторы статического класса и поля), память кучи, используемая для первого экземпляра класса, может включать эти данные. Мы должны игнорировать пространство кучи, используемое экземпляром первого класса.

Учитывая эти проблемы, я представляю Sizeof , инструмент, с помощью которого я отслеживаю различные классы ядра и приложений Java:

Ключевыми методами Sizeof являются runGC() и usedMemory() . Я использую метод-оболочку runGC() для вызова _runGC() несколько раз, потому что это делает метод более агрессивным. (Я не уверен, почему, но возможно создание и уничтожение фрейма стека вызовов методов вызывает изменение в корневом наборе достижимости и побуждает сборщик мусора работать усерднее. Более того, потребление большой доли пространства кучи для создания достаточной работы сборщик мусора также помогает. В общем, трудно гарантировать, что все собрано. Точные детали зависят от JVM и алгоритма сборки мусора.)

Внимательно отметьте места, где я вызываю runGC() . Вы можете отредактировать код между объявлениями кучи1 и кучи2, чтобы создать экземпляр всего, что вас интересует.

Также обратите внимание, как Sizeof выводит размер объекта: транзитивное замыкание данных, необходимых для всех экземпляров класса count, деленное на count . Для большинства классов результатом будет память, потребляемая одним экземпляром класса, включая все принадлежащие ему поля. Это значение объема памяти отличается от данных, предоставляемых многими коммерческими профилировщиками, которые сообщают о неглубоком объеме памяти (например, если объект имеет поле int[], потребление им памяти будет отображаться отдельно).

Результаты

Давайте применим этот простой инструмент к нескольким классам, а затем посмотрим, соответствуют ли результаты нашим ожиданиям.

Примечание. Следующие результаты основаны на JDK 1.3.1 Sun для Windows. Из-за того, что гарантируется и не гарантируется языком Java и спецификациями JVM, вы не можете применить эти конкретные результаты к другим платформам или другим реализациям Java.

java.lang.Объект

Ну, корень всех объектов просто должен был быть моим первым случаем. Для java.lang.Object я получаю:

Итак, обычный объект занимает 8 байт; конечно, никто не должен ожидать, что размер будет равен 0, поскольку каждый экземпляр должен содержать поля, поддерживающие базовые операции, такие как equals() , hashCode() , wait()/notify() и т. д.

java.lang.Целое число

Мы с коллегами часто заключаем нативные целые числа в экземпляры Integer, чтобы хранить их в коллекциях Java. Во сколько нам обходится память?

16-байтовый результат немного хуже, чем я ожидал, потому что значение int может поместиться всего в 4 дополнительных байта. Использование Integer обходится мне в 300 процентов накладных расходов на память по сравнению с тем, когда я могу хранить значение в виде примитивного типа.

java.lang.Long

Long должен занимать больше памяти, чем Integer , но это не так:

Массивы

Использование массивов примитивных типов оказалось поучительным, отчасти для того, чтобы обнаружить любые скрытые накладные расходы, а отчасти для того, чтобы оправдать другой популярный прием: заключить примитивные значения в массив размера 1, чтобы использовать их в качестве объектов. Изменив Sizeof.main(), чтобы иметь цикл, который увеличивает длину созданного массива на каждой итерации, я получаю для массивов int:

и для массивов символов:

Многомерные массивы

Многомерные массивы преподносят еще один сюрприз. Разработчики обычно используют такие конструкции, как int[dim1][dim2] в числовых и научных вычислениях. В экземпляре массива int[dim1][dim2] каждый вложенный массив int[dim2] сам по себе является объектом. Каждый добавляет обычные 16-байтовые служебные данные массива. Когда мне не нужен треугольный или рваный массив, это представляет собой чистые накладные расходы. Влияние возрастает, когда размеры массива сильно различаются. Например, экземпляр int[128][2] занимает 3600 байт. По сравнению с 1040 байтами, используемыми экземпляром int[256] (имеющим такую ​​же емкость), 3600 байтов представляют собой 246-процентные накладные расходы. В крайнем случае byte[256][1] коэффициент накладных расходов составляет почти 19! Сравните это с ситуацией C/C++, в которой тот же синтаксис не увеличивает нагрузку на хранилище.

java.lang.String

Давайте попробуем создать пустую строку String , сначала созданную как new String() :

Результат оказался весьма удручающим. Пустая строка занимает 40 байтов — памяти достаточно для размещения 20 символов Java.

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

не будет работать, потому что все такие дескрипторы объектов в конечном итоге будут указывать на один и тот же экземпляр String. Спецификация языка диктует такое поведение (см. также метод java.lang.String.intern()). Поэтому, чтобы продолжить наше слежение за памятью, попробуйте:

Вооружившись этим методом создания строк, я получаю следующие результаты:

Результаты ясно показывают, что рост памяти String отслеживает рост его внутреннего массива char. Однако класс String добавляет еще 24 байта служебных данных. Для непустой строки размером 10 символов или менее дополнительные накладные расходы относительно полезной нагрузки (2 байта для каждого символа плюс 4 байта для длины) составляют от 100 до 400 процентов.

Конечно, штраф зависит от распределения данных вашего приложения. Почему-то я подозревал, что 10 символов представляют собой типичную длину строки для различных приложений. Чтобы получить конкретную точку данных, я оснастил демонстрацию SwingSet2 (путем непосредственного изменения реализации класса String), поставляемой с JDK 1.3.x, для отслеживания длин создаваемых им строк. После нескольких минут игры с демонстрацией дамп данных показал, что было создано около 180 000 строк. Сортировка их по размерам подтвердила мои ожидания:

Верно, более 50 % всех длин строк попали в сегмент 0–10 — самую горячую точку неэффективности класса String!

На самом деле String может потреблять даже больше памяти, чем предполагает их длина: String , сгенерированные из StringBuffer s (либо явно, либо с помощью оператора конкатенации '+'), скорее всего, имеют массивы символов с длиной, превышающей указанную длину String, потому что StringBuffer обычно начинаются с емкости 16, а затем удваивают ее в операциях append(). Так, например, createString(1) + ' ' заканчивается массивом символов размером 16, а не 2.

Что мы делаем?

"Все это очень хорошо, но у нас нет другого выбора, кроме как использовать String и другие типы, предоставляемые Java, не так ли?" Я слышу, как ты спрашиваешь. Давайте узнаем.

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

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

Примечание. Исходный код этой статьи можно загрузить из ресурсов.

Инструмент

Поскольку Java намеренно скрывает многие аспекты управления памятью, определение того, сколько памяти потребляют ваши объекты, требует определенных усилий. Вы можете использовать метод Runtime.freeMemory() для измерения различий в размерах кучи до и после выделения нескольких объектов. В нескольких статьях, таких как «Вопрос недели № 107» Рамчандера Варадараджана (Sun Microsystems, сентябрь 2000 г.) и «Память имеет значение» Тони Синтеса (JavaWorld, декабрь 2001 г.), подробно излагается эта идея. К сожалению, решение из первой статьи дает сбой, потому что в реализации используется неверный метод среды выполнения, а решение из последней статьи имеет свои недостатки:

  • Одного вызова Runtime.freeMemory() оказывается недостаточно, поскольку JVM может принять решение об увеличении текущего размера кучи в любое время (особенно при сборке мусора).Если общий размер кучи уже не равен максимальному размеру -Xmx, мы должны использовать Runtime.totalMemory()-Runtime.freeMemory() в качестве используемого размера кучи.
  • Выполнение одного вызова Runtime.gc() может оказаться недостаточно агрессивным для запроса сборки мусора. Мы могли бы, например, запросить запуск финализаторов объектов. А поскольку Runtime.gc() не задокументировано для блокировки до завершения сбора, рекомендуется подождать, пока предполагаемый размер кучи не стабилизируется.
  • Если профилируемый класс создает какие-либо статические данные как часть инициализации своего класса для каждого класса (включая инициализаторы статического класса и поля), память кучи, используемая для первого экземпляра класса, может включать эти данные. Мы должны игнорировать пространство кучи, используемое экземпляром первого класса.

Учитывая эти проблемы, я представляю Sizeof , инструмент, с помощью которого я отслеживаю различные классы ядра и приложений Java:

Ключевыми методами Sizeof являются runGC() и usedMemory() . Я использую метод-оболочку runGC() для вызова _runGC() несколько раз, потому что это делает метод более агрессивным. (Я не уверен, почему, но возможно создание и уничтожение фрейма стека вызовов методов вызывает изменение в корневом наборе достижимости и побуждает сборщик мусора работать усерднее. Более того, потребление большой доли пространства кучи для создания достаточной работы сборщик мусора также помогает. В общем, трудно гарантировать, что все собрано. Точные детали зависят от JVM и алгоритма сборки мусора.)

Внимательно отметьте места, где я вызываю runGC() . Вы можете отредактировать код между объявлениями кучи1 и кучи2, чтобы создать экземпляр всего, что вас интересует.

Также обратите внимание, как Sizeof выводит размер объекта: транзитивное замыкание данных, необходимых для всех экземпляров класса count, деленное на count . Для большинства классов результатом будет память, потребляемая одним экземпляром класса, включая все принадлежащие ему поля. Это значение объема памяти отличается от данных, предоставляемых многими коммерческими профилировщиками, которые сообщают о неглубоком объеме памяти (например, если объект имеет поле int[], потребление им памяти будет отображаться отдельно).

Результаты

Давайте применим этот простой инструмент к нескольким классам, а затем посмотрим, соответствуют ли результаты нашим ожиданиям.

Примечание. Следующие результаты основаны на JDK 1.3.1 Sun для Windows. Из-за того, что гарантируется и не гарантируется языком Java и спецификациями JVM, вы не можете применить эти конкретные результаты к другим платформам или другим реализациям Java.

java.lang.Объект

Ну, корень всех объектов просто должен был быть моим первым случаем. Для java.lang.Object я получаю:

Итак, обычный объект занимает 8 байт; конечно, никто не должен ожидать, что размер будет равен 0, поскольку каждый экземпляр должен содержать поля, поддерживающие базовые операции, такие как equals() , hashCode() , wait()/notify() и т. д.

java.lang.Целое число

Мы с коллегами часто заключаем нативные целые числа в экземпляры Integer, чтобы хранить их в коллекциях Java. Во сколько нам обходится память?

16-байтовый результат немного хуже, чем я ожидал, потому что значение int может поместиться всего в 4 дополнительных байта. Использование Integer обходится мне в 300 процентов накладных расходов на память по сравнению с тем, когда я могу хранить значение в виде примитивного типа.

java.lang.Long

Long должен занимать больше памяти, чем Integer , но это не так:

Массивы

Использование массивов примитивных типов оказалось поучительным, отчасти для того, чтобы обнаружить любые скрытые накладные расходы, а отчасти для того, чтобы оправдать другой популярный прием: заключить примитивные значения в массив размера 1, чтобы использовать их в качестве объектов. Изменив Sizeof.main(), чтобы иметь цикл, который увеличивает длину созданного массива на каждой итерации, я получаю для массивов int:

и для массивов символов:

Многомерные массивы

Многомерные массивы преподносят еще один сюрприз. Разработчики обычно используют такие конструкции, как int[dim1][dim2] в числовых и научных вычислениях. В экземпляре массива int[dim1][dim2] каждый вложенный массив int[dim2] сам по себе является объектом. Каждый добавляет обычные 16-байтовые служебные данные массива. Когда мне не нужен треугольный или рваный массив, это представляет собой чистые накладные расходы. Влияние возрастает, когда размеры массива сильно различаются. Например, экземпляр int[128][2] занимает 3600 байт. По сравнению с 1040 байтами, используемыми экземпляром int[256] (имеющим такую ​​же емкость), 3600 байтов представляют собой 246-процентные накладные расходы. В крайнем случае byte[256][1] коэффициент накладных расходов составляет почти 19! Сравните это с ситуацией C/C++, в которой тот же синтаксис не увеличивает нагрузку на хранилище.

java.lang.String

Давайте попробуем создать пустую строку String , сначала созданную как new String() :

Результат оказался весьма удручающим. Пустая строка занимает 40 байтов — памяти достаточно для размещения 20 символов Java.

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

не будет работать, потому что все такие дескрипторы объектов в конечном итоге будут указывать на один и тот же экземпляр String. Спецификация языка диктует такое поведение (см. также метод java.lang.String.intern()). Поэтому, чтобы продолжить наше слежение за памятью, попробуйте:

Вооружившись этим методом создания строк, я получаю следующие результаты:

Результаты ясно показывают, что рост памяти String отслеживает рост его внутреннего массива char. Однако класс String добавляет еще 24 байта служебных данных. Для непустой строки размером 10 символов или менее дополнительные накладные расходы относительно полезной нагрузки (2 байта для каждого символа плюс 4 байта для длины) составляют от 100 до 400 процентов.

Конечно, штраф зависит от распределения данных вашего приложения. Почему-то я подозревал, что 10 символов представляют собой типичную длину строки для различных приложений. Чтобы получить конкретную точку данных, я оснастил демонстрацию SwingSet2 (путем непосредственного изменения реализации класса String), поставляемой с JDK 1.3.x, для отслеживания длин создаваемых им строк. После нескольких минут игры с демонстрацией дамп данных показал, что было создано около 180 000 строк. Сортировка их по размерам подтвердила мои ожидания:

Верно, более 50 % всех длин строк попали в сегмент 0–10 — самую горячую точку неэффективности класса String!

На самом деле String может потреблять даже больше памяти, чем предполагает их длина: String , сгенерированные из StringBuffer s (либо явно, либо с помощью оператора конкатенации '+'), скорее всего, имеют массивы символов с длиной, превышающей указанную длину String, потому что StringBuffer обычно начинаются с емкости 16, а затем удваивают ее в операциях append(). Так, например, createString(1) + ' ' заканчивается массивом символов размером 16, а не 2.

Что мы делаем?

"Все это очень хорошо, но у нас нет другого выбора, кроме как использовать String и другие типы, предоставляемые Java, не так ли?" Я слышу, как ты спрашиваешь. Давайте узнаем.

Пустая строка занимает 40 байтов — достаточно памяти для размещения 20 символов Java. Результаты ясно показывают, что рост памяти String отслеживает рост его внутреннего массива char. Однако класс String добавляет еще 24 байта служебной информации.

Сколько памяти занимает строка C?

В результате каждая строка занимает 20 байт служебных данных, а затем по 2 байта на каждый символ в своем буфере. Поскольку каждая строка состоит из десяти символов, для одной строки потребуется 40 байт, а для 100 строк потребуется 4000 байт.

В чем разница между строкой и символом в C?

Основное различие между символом и строкой заключается в том, что символ означает одну букву, цифру, пробел, знак препинания или символ, который может быть представлен с помощью компьютера, а строка — набор символов. В программировании на C мы можем использовать тип данных char для хранения как символьных, так и строковых значений.

Сколько байтов занимает строковый символ?

Какой тип данных возвращает sizeof?

Возвращает размер переменной. Его можно применять к любому типу данных, типу с плавающей запятой, переменным типа указателя. Когда sizeof() используется с типами данных, он просто возвращает объем памяти, выделенный для этого типа данных.

Какой тип sizeof возвращает C++?

Вся необходимая информация — это возвращаемый тип функции, первый sizeof вернет размер шорта (тип возвращаемого значения функции) в виде значения 2 (в size_t целочисленный тип, определенный во включаемом файле STDDEF. H), а второй sizeof вернет 4 (размер size_t, возвращенный первым sizeof).

Что возвращает функция sizeof() в C++?

Оператор sizeof() в C++ Sizeof() — это оператор, который оценивает размер типа данных, констант и переменных. Это оператор времени компиляции, поскольку он возвращает размер любой переменной или константы во время компиляции.

Какой размер указателя ()?

Поскольку один байт равен восьми битам, 64 бита / 8 = 8 представляет собой размер указателя. На 32-битных машинах указатели соответственно занимают 32 бита / 8 = 4 байта.

Оглавление

Сколько памяти занимает строка?

Строка. Результат оказывается весьма удручающим. Пустая строка занимает 40 байтов — памяти достаточно для размещения 20 символов Java.

Что такое выделение памяти для строки?

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

Где хранится строка в Java?

область кучи
В Java строки хранятся в области кучи.

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

Для каждого экземпляра класса, который вы создаете (и который не был собран с помощью мусора), вы можете приблизительно определить объем памяти, суммируя использование памяти каждой объявленной переменной на основе экземпляра… (поле) ссылочные переменные (ссылки на другие объекты) занимают 4 или 8 байт (32/64-битная ОС?) int16, Int32, Int64 занимают 2,4 или 8 байт соответственно…

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

Из-за того, что большинство вычислений Spark выполняются в памяти, программы Spark могут быть ограничены любым ресурсом в кластере: ЦП, пропускной способностью сети или памятью. Чаще всего, если данные помещаются в память, узким местом является пропускная способность сети, но иногда вам также необходимо выполнить некоторую настройку, например, хранить RDD в сериализованной форме, чтобы уменьшить использование памяти. В этом руководстве рассматриваются две основные темы: сериализация данных, которая имеет решающее значение для хорошей производительности сети и может также сократить использование памяти, и настройка памяти. Мы также набросаем несколько небольших тем.

Сериализация играет важную роль в производительности любого распределенного приложения. Форматы, которые медленно сериализуют объекты или потребляют большое количество байтов, значительно замедляют вычисления. Часто это будет первое, что вы должны настроить для оптимизации приложения Spark. Spark стремится найти баланс между удобством (позволяя вам работать с любым типом Java в ваших операциях) и производительностью. Он предоставляет две библиотеки сериализации:

    : по умолчанию Spark сериализует объекты, используя инфраструктуру Java ObjectOutputStream, и может работать с любым созданным вами классом, который реализует java.io.Serializable. Вы также можете более точно контролировать производительность вашей сериализации, расширив java.io.Externalizable . Сериализация в Java является гибкой, но часто довольно медленной и приводит к большим сериализованным форматам для многих классов. : Spark также может использовать библиотеку Kryo (версия 2) для более быстрой сериализации объектов. Kryo значительно быстрее и компактнее, чем сериализация Java (часто до 10 раз), но не поддерживает все сериализуемые типы и требует от вас регистрировать классы, которые вы будете использовать в программе, заранее для наилучшего результата. представление.

Вы можете переключиться на использование Kryo, инициализировав задание с помощью SparkConf и вызвав conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") . Этот параметр настраивает сериализатор, используемый не только для перетасовки данных между рабочими узлами, но и для сериализации RDD на диск. Единственная причина, по которой Kryo не используется по умолчанию, заключается в требованиях к пользовательской регистрации, но мы рекомендуем попробовать его в любом приложении с интенсивным использованием сети.

Наконец, чтобы зарегистрировать свои классы в Kryo, создайте общедоступный класс, который расширяет org.apache.spark.serializer.KryoRegistrator, и установите свойство конфигурации spark.kryo.registrator, чтобы оно указывало на него, как показано ниже:

В документации Kryo описаны дополнительные параметры регистрации, такие как добавление пользовательского кода сериализации.

Если ваши объекты большие, вам также может понадобиться увеличить свойство конфигурации spark.kryoserializer.buffer.mb. Значение по умолчанию — 2, но это значение должно быть достаточно большим, чтобы вместить самый большой объект, который вы будете сериализовать.

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

При настройке использования памяти необходимо учитывать три фактора: количество памяти, используемой вашими объектами (вы можете захотеть, чтобы весь набор данных умещался в памяти), стоимость доступ к этим объектам, а также накладные расходы на сборку мусора (если у вас большой оборот с точки зрения объектов).

По умолчанию объекты Java доступны быстро, но могут легко занимать в 2–5 раз больше места, чем «сырые» данные внутри их полей. Это связано с несколькими причинами:

  • Каждый отдельный объект Java имеет «заголовок объекта» размером около 16 байт и содержит такую ​​информацию, как указатель на его класс. Для объекта с очень небольшим количеством данных (например, с одним полем Int) это может быть больше, чем данные.
  • Строки Java имеют около 40 байт служебных данных по сравнению с необработанными строковыми данными (поскольку они хранят их в массиве Char и сохраняют дополнительные данные, такие как длина), и сохраняют каждый символ как два байт из-за Unicode. Таким образом, строка из 10 символов легко может занимать 60 байт.
  • Обычные классы коллекций, такие как HashMap и LinkedList , используют связанные структуры данных, где для каждой записи существует объект-оболочка (например, Map.Entry ). Этот объект имеет не только заголовок, но и указатели (обычно по 8 байт каждый) на следующий объект в списке.
  • Наборы типов-примитивов часто хранятся в виде "коробочных" объектов, таких как java.lang.Integer .

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

Определение потребления памяти

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

Это означает, что раздел 1 RDD 0 занял 717,5 КБ.

Настройка структур данных

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

  1. Создавайте свои структуры данных, предпочитая массивы объектов и примитивные типы вместо стандартных классов коллекций Java или Scala (например, HashMap ). Библиотека fastutil предоставляет удобные классы коллекций для примитивных типов, совместимых со стандартной библиотекой Java.
  2. По возможности избегайте вложенных структур с большим количеством мелких объектов и указателей.
  3. Рассмотрите возможность использования числовых идентификаторов или объектов перечисления вместо строк для ключей.
  4. Если у вас меньше 32 ГБ ОЗУ, установите флаг JVM -XX:+UseCompressedOops, чтобы указатели были четырехбайтными вместо восьми. Кроме того, в Java 7 или более поздних версиях попробуйте -XX:+UseCompressedStrings, чтобы хранить строки ASCII как всего 8 бит на символ. Вы можете добавить эти параметры в spark-env.sh .

Сериализованное хранилище RDD

Если ваши объекты по-прежнему слишком велики для эффективного хранения, несмотря на эту настройку, гораздо более простой способ сократить использование памяти — хранить их в сериализованной форме с использованием сериализованных уровней StorageLevels в API сохраняемости RDD. например MEMORY_ONLY_SER . Затем Spark будет хранить каждый раздел RDD как один большой массив байтов. Единственным недостатком хранения данных в сериализованной форме является более медленное время доступа из-за необходимости десериализовать каждый объект на лету. Мы настоятельно рекомендуем использовать Kryo, если вы хотите кэшировать данные в сериализованной форме, так как это приводит к гораздо меньшим размерам, чем сериализация Java (и, конечно, необработанные объекты Java).

Настройка сборки мусора

Сборка мусора JVM может быть проблемой, если у вас есть большой "переход" с точки зрения RDD, хранящихся в вашей программе. (Обычно это не проблема в программах, которые просто считывают RDD один раз, а затем выполняют над ним много операций.) Когда Java нужно вытеснить старые объекты, чтобы освободить место для новых, ей нужно будет проследить все ваши объекты Java и найти неиспользованные. Здесь важно помнить, что стоимость сборки мусора пропорциональна количеству объектов Java, поэтому использование структур данных с меньшим количеством объектов (например, массивов Int вместо LinkedList ) значительно снижает это стоимость. Еще лучший метод — сохранять объекты в сериализованной форме, как описано выше: теперь будет только один объект (массив байтов) на раздел RDD. Прежде чем пробовать другие методы, первое, что нужно попробовать, если GC вызывает проблемы, — это использовать сериализованное кэширование.

GC также может быть проблемой из-за помех между рабочей памятью ваших задач (объемом пространства, необходимого для запуска задачи) и RDD, кэшированными на ваших узлах. Мы обсудим, как контролировать пространство, выделенное для кэша RDD, чтобы избежать этого.

Измерение влияния GC

Первым этапом настройки сборщика мусора является сбор статистики о том, как часто выполняется сборка мусора и сколько времени затрачивается сборщик мусора. Это можно сделать, добавив -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps в переменную среды SPARK_JAVA_OPTS. В следующий раз, когда ваше задание Spark будет запущено, вы увидите сообщения, печатаемые в журналах рабочего процесса каждый раз, когда происходит сборка мусора. Обратите внимание, что эти журналы будут находиться на рабочих узлах вашего кластера (в файлах stdout в их рабочих каталогах), не в вашей программе-драйвере.

Настройка размера кэша

Один из важных параметров конфигурации для GC — это объем памяти, который следует использовать для кэширования RDD. По умолчанию Spark использует 66 % настроенной памяти исполнителя ( spark.executor.memory или SPARK_MEM ) для кэширования RDD. Это означает, что для любых объектов, созданных во время выполнения задачи, доступно 33 % памяти.

Если ваши задачи замедляются и вы обнаружите, что ваша JVM часто выполняет сборку мусора или не хватает памяти, уменьшение этого значения поможет снизить потребление памяти. Чтобы изменить это значение на 50%, вы можете вызвать conf.set("spark.storage.memoryFraction", "0.5") на вашем SparkConf. В сочетании с использованием сериализованного кэширования использование кэша меньшего размера должно быть достаточным для смягчения большинства проблем со сборкой мусора. Если вы заинтересованы в дальнейшей настройке Java GC, продолжайте читать ниже.

Расширенная настройка GC

Для дальнейшей настройки сборки мусора нам сначала нужно понять некоторые основные сведения об управлении памятью в JVM:

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

Молодое поколение делится на три региона [Эдем, Выживший1, Выживший2].

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

В распечатываемой статистике GC, если OldGen близок к заполнению, уменьшите объем памяти, используемый для кэширования. Это можно сделать с помощью свойства spark.storage.memoryFraction. Лучше кэшировать меньше объектов, чем замедлять выполнение задачи!

Если второстепенных коллекций слишком много, а основных сборщиков мусора мало, можно выделить больше памяти для Eden. Вы можете установить размер Эдема так, чтобы он был завышенной оценкой того, сколько памяти потребуется для каждой задачи. Если размер Эдема определен как E , то вы можете установить размер поколения Young с помощью опции -Xmn=4/3*E . (Увеличение на 4/3 должно также учитывать пространство, используемое выжившими областями.)

Например, если ваша задача считывает данные из HDFS, объем памяти, используемый задачей, можно оценить, используя размер блока данных, считанного из HDFS. Обратите внимание, что размер распакованного блока часто в 2 или 3 раза превышает размер блока. Таким образом, если мы хотим иметь рабочее пространство на 3 или 4 задачи, а размер блока HDFS составляет 64 МБ, мы можем оценить размер Eden как 4*3*64 МБ .

Следите за тем, как частота и время, затрачиваемое на сборку мусора, меняются с новыми настройками.

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

Уровень параллелизма

Кластеры не будут использоваться полностью, если вы не установите достаточно высокий уровень параллелизма для каждой операции. Spark автоматически устанавливает количество задач «сопоставления» для каждого файла в соответствии с его размером (хотя вы можете управлять им с помощью дополнительных параметров SparkContext.textFile и т. д.), а также для распределенных операций «уменьшения», таких как groupByKey и reduceByKey, он использует наибольшее количество разделов родительского RDD. Вы можете передать уровень параллелизма в качестве второго аргумента (см. документацию spark.PairRDDFunctions) или установить свойство конфигурации spark.default.parallelism, чтобы изменить значение по умолчанию. Как правило, мы рекомендуем 2–3 задачи на ядро ​​ЦП в вашем кластере.

Использование памяти в задачах сокращения

Иногда вы получаете ошибку OutOfMemoryError не потому, что ваши RDD не помещаются в памяти, а потому, что рабочий набор одной из ваших задач, например одной из задач сокращения в groupByKey , был слишком большим. Операции перетасовки Spark ( sortByKey , groupByKey , reduceByKey , join и т. д.) создают хеш-таблицу внутри каждой задачи для выполнения группировки, которая часто может быть большой. Самое простое решение здесь — увеличить уровень параллелизма, чтобы входной набор каждой задачи был меньше. Spark может эффективно поддерживать задачи длительностью до 200 мс, поскольку он повторно использует одну рабочую JVM для всех задач и имеет низкую стоимость запуска задачи, поэтому вы можете безопасно увеличить уровень параллелизма до числа, превышающего количество ядер в ваших кластерах.< /p>

Передача больших переменных

Использование функции широковещательной рассылки, доступной в SparkContext, может значительно уменьшить размер каждой сериализованной задачи и стоимость запуска задания в кластере. Если ваши задачи используют какой-либо большой объект из программы-драйвера внутри себя (например, статическую таблицу поиска), подумайте о том, чтобы превратить его в широковещательную переменную. Spark печатает сериализованный размер каждой задачи на мастере, поэтому вы можете посмотреть на это, чтобы решить, не слишком ли велики ваши задачи; обычно задачи размером более 20 КБ, вероятно, заслуживают оптимизации.

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

Читайте также: