Подтвердить что ты не робот

Есть ли эффективная замена памяти java.lang.String?

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

length: 0, {class java.lang.String} size = 40 bytes
length: 7, {class java.lang.String} size = 56 bytes

Хотя в статье есть несколько советов, чтобы свести к минимуму это, я не нашел их полностью удовлетворительными. Кажется, расточительно использовать char[] для хранения данных. Очевидным улучшением для большинства западных языков было бы использовать byte[] и такую ​​кодировку, как UTF-8, так как вам нужен только один байт для хранения наиболее частых символов, а не двух байтов.

Конечно, можно использовать String.getBytes("UTF-8") и new String(bytes, "UTF-8"). Даже накладные расходы на экземпляр String исчезнут. Но тогда вы теряете очень удобные методы, такие как equals(), hashCode(), length(),...

Насколько я могу сказать, Sun имеет patent в byte[] представлении строк.

Рамки для эффективного представления строковых объектов в средах программирования Java
... Методы могут быть реализованы для создания строковых объектов Java как массивы однобайтовых символов, когда это подходит...

Но мне не удалось найти API для этого патента.

Почему меня это волнует? В большинстве случаев я этого не делаю. Но я работал над приложениями с огромными кешами, содержащими множество строк, которые могли бы извлечь выгоду из использования памяти более эффективно.

Знает ли кто-нибудь об этом API? Или есть ли другой способ сохранить объем памяти для строк небольшим, даже ценой производительности процессора или более уродливым API?

Пожалуйста, не повторяйте предложения из вышеуказанной статьи:

  • собственный вариант String.intern() (возможно, с SoftReferences)
  • сохранение единственного char[] и использование текущей реализации String.subString(.) во избежание копирования данных (противный)

Обновление

Я запустил код из статьи о Sun JVM (1.6.0_10). Это дало те же результаты, что и в 2002 году.

4b9b3361

Ответ 1

С небольшим количеством помощи от JVM...

ПРЕДУПРЕЖДЕНИЕ: Это решение теперь устарело в новых версиях Java SE. См. Другие решения ad-hoc ниже.

Если вы используете JVM HotSpot, так как обновление Java 6 21, вы можете использовать эту опцию командной строки:

-XX:+UseCompressedStrings

Страница Параметры JVM читает:

Используйте байт [] для строк, которые могут быть представлены как чистый ASCII. (Введен в редакторе выпусков Java 6 Update 21)

UPDATE. Эта функция была нарушена в более поздней версии и должна была быть исправлена ​​снова в Java SE 6u25, как указано 6u25 b03 примечания к выпуску (однако мы не видим его в 6u25 final release notes). отчет об ошибке 7016213 не отображается по соображениям безопасности. Поэтому сначала используйте осторожность и проверьте. Как и любой параметр -XX, он считается экспериментальным и может быть изменен без особого внимания, поэтому, вероятно, не всегда лучше не использовать его в сценарии запуска рабочего сервера.

UPDATE 2013-03 (спасибо комментарию Алексей Максимус): Смотрите и принятый ответ. Теперь вариант кажется умершим. Это также подтверждается в сообщении об ошибке 7129417.

Конец Обосновывает средства

Предупреждение: (Ugly) решения для конкретных потребностей

Это немного из коробки и более низкого уровня, но поскольку вы спросили... не попадайте в посланника!

Ваше собственное строковое представление строки

Если ASCII подходит для вас, то почему бы вам просто не запустить свою собственную реализацию?

Как вы уже упоминали, вы могли byte[] вместо char[] внутренне. Но это не все.

Чтобы сделать это еще более легким, вместо того, чтобы обертывать ваши байтовые массивы в классе, почему бы просто не использовать вспомогательный класс, содержащий в основном статические методы, работающие на этих байтовых массивах, которые вы проходите? Конечно, он будет чувствовать себя довольно C-ish, но он будет работать и спасет вам огромные служебные данные, которые идут с объектами String.

И конечно, это пропустит некоторые приятные функциональные возможности... если вы их повторно не реализуете. Если вы действительно в них нуждаетесь, тогда выбора не будет. Благодаря OpenJDK и множеству других хороших проектов вы можете очень эффективно развернуть свой собственный fugly класс LiteStrings, который работает только с параметрами byte[]. Вы будете чувствовать, что принимаете душ каждый раз, когда вам нужно вызвать функцию, но вы сохраните кучу памяти.

Я бы рекомендовал сделать его похожим на контракт класса String и предоставить значимые адаптеры и сборщики для преобразования из и в String, и вы также можете иметь адаптеры с StringBuffer и StringBuilder, а также некоторые зеркальные реализации других вещей, которые могут вам понадобиться. Определенно какая-то часть работы, но может стоить того (см. Немного ниже раздела "Сделать это!" ).

Сжатие/декомпрессия на лету

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

Конечно, быть жестоким будет означать:

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

Сделайте оба

Для полной головной боли, конечно, вы можете сделать все это:

  • Класс х-х-х-х,
  • массивы байтов,
  • сжатый накопитель "на лету".

Обязательно сделайте это с открытым исходным кодом.:)

Сделайте это Count!

Кстати, см. эту замечательную презентацию на Создание памяти-эффективных приложений Java Н. Митчелл и Г. Севицкий: [Версия 2008 года, [версия 2009 года.

В этой презентации мы видим, что строка 8- char использует 64 байта в 32-разрядной системе (96 для 64-битной системы!), и большая ее часть из-за накладных расходов JVM. И из этой статьи мы видим, что 8-байтовый массив будет "только" 24 байта: 12 байтов заголовка, 8 x 1 байт + 4 байта выравнивания).

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

Ответ 2

В Terracotta мы имеем некоторые случаи, когда мы сжимаем большие строки, когда они отправляются по сети, и фактически оставляют их сжатыми до тех пор, пока не потребуется декомпрессия. Мы делаем это, преобразовывая char [] в байт [], сжимая байт [], затем кодируя этот байт [] обратно в исходное char []. Для некоторых операций, таких как хеш и длина, мы можем ответить на эти вопросы без декодирования сжатой строки. Для данных, таких как большие строки XML, вы можете получить существенное сжатие таким образом.

Перемещение сжатых данных по сети - определенная победа. Хранение его в сжатом состоянии зависит от варианта использования. Конечно, у нас есть некоторые кнопки, чтобы отключить это и изменить длину, с которой включается сжатие, и т.д.

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

Ответ 3

В статье указывается на две вещи:

  • Массивы символов увеличиваются в кусках по 8 байт.
  • Существует большая разница в размере между объектами char [] и String.

Накладные расходы связаны с включением ссылки на объект char [] и тремя целями: смещением, длиной и пространством для хранения хеш-кода String, а также стандартными издержками просто быть объектом.

Немного отличается от String.intern() или массива символов, используемого String.substring(), используя один char [] для всех строк, это означает, что вам не нужно хранить ссылку на объект в вашей обертке Строковый объект. Вам все равно потребуется смещение, и вы введете (большое) ограничение на количество символов, которое вы можете получить в целом.

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

Компонент пространства-времени, не сохраняющий хэш, может помочь вам, если вам это не нужно часто.

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

Кроме того, я мог оставить входные данные в массиве байтов, из которого он был первоначально прочитан, - файл с отображением памяти.

Мои объекты состояли из смещения int (предел соответствовал моей ситуации), int length и int hashcode.

java.lang.String был знакомым молотом для того, что я хотел сделать, но не лучшим инструментом для работы.

Ответ 4

Я думаю, вы должны очень осторожно относиться к идеям и/или предположениям из статьи javaworld.com с 2002 года. За шесть лет с тех пор было много изменений в компиляторе и JVM. По крайней мере, сначала проверьте свою гипотезу и решение против современной JVM, чтобы убедиться, что решение даже стоит усилий.

Ответ 5

Внутренняя кодировка UTF-8 имеет свои преимущества (например, меньший объем памяти, который вы указали), но имеет и недостатки.

Например, определение длины символа (а не длины байта) кодированной строки UTF-8 является операцией O (n). В строке java стоимость определения длины символа равна O (1), а генерация UTF-8 представляет собой O (n).

Все о приоритетах.

Структура структуры данных часто может рассматриваться как компромисс между скоростью и пространством. В этом случае, я думаю, разработчики Java string API сделали выбор на основе этих критериев:

  • Класс String должен поддерживать все возможные символы юникода.

  • Хотя unicode определяет 1 байтовый, 2 байтовый и 4-байтовый варианты, 4-байтовые символы (на практике) довольно редки, поэтому их можно представить как суррогатные пары. Поэтому Java использует 2-байтовый примитив char.

  • Когда люди называют методы length(), indexOf() и charAt(), они заинтересованы в позиции символа, а не в позиции байта. Чтобы создавать быстрые реализации этих методов, необходимо избегать внутренней кодировки UTF-8.

  • Языки, подобные С++, усложняют жизнь программиста, определяя три разных типа символов и заставляя программиста выбирать между ними. Большинство программистов начинают использовать простые строки ASCII, но когда им в конечном итоге необходимо поддерживать международные символы, процесс изменения кода для использования многобайтовых символов крайне болезнен. Я думаю, что разработчики Java сделали отличный компромиссный выбор, сказав, что все строки состоят из 2-байтных символов.

Ответ 6

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

Единственные другие реализации String, о которых я знаю, - это те, что содержатся в классах Javolution. Я не думаю, что они более эффективны с точки зрения памяти:

http://www.javolution.com/api/javolution/text/Text.html
http://www.javolution.com/api/javolution/text/TextBuilder.html

Ответ 7

Java выбрала UTF-16 для компромисса скорости и размера хранилища. Обработка данных UTF-8 намного больше PITA, чем обработка данных UTF-16 (например, при попытке найти положение символа X в массиве байтов, как вы будете делать это быстро, если каждый символ может иметь один, два, три или даже до шести байтов? Когда-либо думали об этом? Перечисление байта строки байтом не очень быстро, понимаете?). Конечно, UTF-32 будет проще всего обрабатывать, но в два раза меньше места для хранения. Все изменилось со времен раннего Юникода. Теперь некоторым символам требуется 4 байта, даже если используется UTF-16. Обработка этих данных делает UTF-16 почти столь же плохим, как UTF-8.

В любом случае, будьте уверены, что если вы реализуете класс String с внутренним хранилищем, использующим UTF-8, вы можете выиграть некоторую память, но вы потеряете скорость обработки для многих строковых методов. Также ваш аргумент - слишком ограниченная точка зрения. Ваш аргумент будет недействительным для кого-то из Японии, поскольку японские символы не будут меньше в UTF-8, чем в UTF-16 (фактически они будут принимать 3 байта в UTF-8, тогда как в UTF-16 они всего два байта), Я не понимаю, почему программисты в таком глобальном мире, как сегодня, с вездесущим Интернетом, все еще говорят о "западных языках", как будто это все, что можно было бы считать, как будто только у западного мира есть компьютеры, а остальная часть жизни пещеры. Рано или поздно любое приложение укусается тем фактом, что он не может эффективно обрабатывать незападные символы.

Ответ 8

Есть накладные расходы на создание объекта (по крайней мере, таблицу рассылки), накладные расходы на то, что он использует 2 байта на письмо и накладные расходы на несколько дополнительных переменных, которые созданы для фактического улучшения скорости и использование памяти во многих случаях.

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

Для ответа, кроме очевидного (то есть, если использование памяти важно, вы, вероятно, должны использовать C), вы можете реализовать свои собственные строки с внутренним представлением в байтовых массивах BCD.

На самом деле это звучит весело, я могу сделать это просто для пинков:)

Массив Java принимает 2 байта на элемент. Закодированная цифра BCD принимает 6 бит на букву IIRC, делая ваши строки значительно меньшими. Вовремя будет небольшая стоимость конвертации, но на самом деле это не так уж плохо. По-настоящему большая проблема заключается в том, что вам нужно будет преобразовать в строку, чтобы что-то сделать с ней.

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

Наконец, заметка. Я полностью против использования такого типа, если у вас нет 3 вещи:

  • Реализация сделана наиболее читаемым способом.
  • Результаты тестов и требования, показывающие, как эта реализация не соответствует требованиям.
  • Результаты тестирования того, как "улучшенная" реализация соответствует требованиям.

Без всех трех из них я бы выбрал любое оптимизированное решение, представленное разработчиком.

Ответ 9

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

  • Разделите строку на 4-символьные "слова" (если вам нужен весь Unicode) и сохраните эти байты в long с помощью маскирования/смещения бит. Если вам не нужен полный набор Unicode и только 255 символов ASCII, вы можете поместить 8 символов в каждый long. Добавьте (char) 0 в конец строки до тех пор, пока длина не разделится равномерно на 4 (или 8).
  • Переопределите реализацию хэш-набора (например, Trove TLongHashSet) и добавьте каждое "слово" к этому набору, скомпилировав массив внутренних индексов, где заканчивается long в наборе (убедитесь, что вы также обновили свой индекс при повторном наборе набора)
  • Для хранения этих индексов используйте двухмерный массив int (поэтому первое измерение представляет собой каждую сжатую строку, а второе измерение - это каждый "индекс слова" в хэш-наборе) и возвращает единственный индекс int в этот массив обратно к вызывающему абоненту (вы должны владеть массивами слов, чтобы вы могли глобально обновлять индекс при переименовании, как упоминалось выше).

Преимущества:

  • Постоянное сжатие/декомпозиция времени
  • Строка длиной n представлена ​​в виде массива int длины n/4 с дополнительными накладными расходами набора слов long, которое растет асимптотически, поскольку встречается меньше уникальных "слов"
  • Пользователь возвращает один int string "ID", который удобен и мал для хранения в своих объектах

Distadvantages:

  • Немного хакерский, поскольку он включает в себя смещение бит, возиться с внутренними элементами хеш-набора и т.д. (Bill K не одобряет)
  • Хорошо работает, когда вы не ожидаете много повторяющихся строк. Очень дорого проверить, существует ли строка в библиотеке.

Ответ 10

Сегодня (2010) каждый ГБ, который вы добавляете на сервер, стоит около 80 или 120 долларов США. Прежде чем переходить к реинжинирингу String, вы должны спросить себя, что это действительно того стоит.

Если вы собираетесь сохранить ГБ памяти, возможно. Десять ГБ, определенно. Если вы хотите сохранить 10 МБ, вы, скорее всего, будете использовать больше времени, чем его ценность.

Как вы компактны, Строки действительно зависят от вашего шаблона использования. Много ли повторяющихся строк? (используйте пул объектов) Много ли длинных строк? (используйте сжатие/кодирование)

Другая причина, по которой вам могут понадобиться меньшие строки, - уменьшить использование кеша. Даже самые большие процессоры имеют около 8 МБ - 12 МБ кэша. Это может быть более ценным ресурсом и не просто увеличиваться. В этом случае я предлагаю вам взглянуть на альтернативы строкам, но вы должны иметь в виду, насколько они будут отличаться в £ или $от времени, которое требуется.

Ответ 11

Параметр компилятора UseCompressedStrings выглядит как самый простой маршрут. Если вы используете строки только для хранения и не выполняете никаких операций equals/substring/split, тогда может быть что-то вроде этого класса CompactCharSequence:

http://www.javamex.com/tutorials/memory/ascii_charsequence.shtml

Ответ 12

Из любопытства стоит несколько байтов, которые действительно стоят того?

Как правило, я предлагаю канавку строк по соображениям производительности, в пользу StringBuffer (помните, строки неизменяемы).

Вы серьезно истощаете свою кучу из ссылок на строки?

Ответ 13

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

Ответ 14

Вы сказали, что не повторите предложение статьи о том, чтобы перевернуть свою собственную схему интернирования, но что не так с String.intern? В статье содержится следующее броское замечание:

Существует множество причин, чтобы избежать метода String.intern(). Во-первых, несколько современных JVM могут обрабатывать большие объемы данных.

Но даже если цифры использования памяти с 2002 года продолжаются шесть лет спустя, я был бы удивлен, если бы не было достигнуто никакого прогресса в отношении того, сколько JVM данных могут ставить стаж.

Это не чисто риторический вопрос - мне интересно знать, есть ли веские причины, чтобы избежать этого. Является ли он реализован неэффективно для многопоточного использования? Заполняет ли он какую-то специфическую область кучи JVM? У вас действительно есть сотни мегабайт уникальных строк (так что интернирование было бы бесполезным в любом случае)?

Ответ 15

Помните, что существует много типов сжатия. Использование кодирования huffman - хороший подход общего назначения, но он относительно интенсивен. Для реализации B + Tree я работал несколько лет назад, мы знали, что ключи, вероятно, будут иметь общие ведущие символы, поэтому мы реализовали алгоритм сжатия символов для каждой страницы в дереве B+. Код был простым, очень быстрым и привел к использованию памяти на 1/3 того, с чего мы начали. В нашем случае настоящая причина для этого заключалась в том, чтобы сэкономить место на диске и сократить время, затрачиваемое на передачу дисков → ОЗУ (и что 1/3 сбережений сильно повлияли на эффективность работы диска).

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

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