Мое приложение является многопоточным с интенсивной Строковой обработкой. Мы испытываем чрезмерное потребление памяти, и профилирование продемонстрировало, что это должно Представить данные в виде строки. Я думаю, что потребление памяти извлекло бы выгоду значительно из использования некоторой реализации шаблона в наилегчайшем весе или даже кэша (я знаю наверняка, что Строки часто дублируются, хотя у меня нет точных данных в том отношении).
Я посмотрел на Java Постоянный Пул и String.intern, но кажется, что он может вызвать некоторые проблемы PermGen.
Какова была бы лучшая альтернатива для реализации многопоточного пула всего приложения Строк в Java?
Править: Также посмотрите мой предыдущий, связанный вопрос: Как Java реализует шаблон в наилегчайшем весе для строки под капотом?
Примечание: В этом ответе используются примеры, которые могут быть неактуальны в современных исполняемых библиотеках JVM. В частности, пример substring
больше не является проблемой в OpenJDK/Oracle 7+.
Я знаю, что это противоречит тому, что вам часто говорят, но иногда явное создание новых экземпляров String
может быть существенным способом уменьшить объем памяти.
Поскольку строки неизменяемы, некоторые методы используют этот факт и совместно используют массив символов подложки для экономии памяти. Однако иногда это может фактически увеличить память за счет предотвращения сборки мусора неиспользуемых частей этих массивов.
Например, предположим, вы разбираете идентификаторы сообщений в файле журнала, чтобы извлечь идентификаторы предупреждений. Ваш код выглядел бы примерно так:
//Format:
//ID: [WARNING|ERROR|DEBUG] Message...
String testLine = "5AB729: WARNING Some really really really long message";
Matcher matcher = Pattern.compile("([A-Z0-9]*): WARNING.*").matcher(testLine);
if ( matcher.matches() ) {
String id = matcher.group(1);
//...do something with id...
}
Но посмотрите на данные, которые на самом деле хранятся:
//...
String id = matcher.group(1);
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] data = ((char[])valueField.get(id));
System.out.println("Actual data stored for string \"" + id + "\": " + Arrays.toString(data) );
Это вся строка test, потому что matcher просто оборачивает новый экземпляр String вокруг тех же символьных данных. Сравните результаты, когда вы замените String id = matcher.group(1);
на String id = new String(matcher.group(1));
.
Это уже сделано на уровне JVM. Вам нужно только убедиться, что вы не создаете new String
каждый раз явно или неявно.
Т.е. не делайте:
String s1 = new String("foo");
String s2 = new String("foo");
Это создаст два экземпляра в куче. Скорее сделайте так:
String s1 = "foo";
String s2 = "foo";
Это создаст один экземпляр в куче, и оба будут ссылаться на одно и то же (в качестве доказательства, s1 == s2
вернет здесь true
).
Также не используйте + =
для объединения строк (в цикле):
String s = "";
for (/* some loop condition */) {
s += "new";
}
+ =
неявно создает новую строку
в куча каждый раз. Скорее сделайте это
StringBuilder sb = new StringBuilder();
for (/* some loop condition */) {
sb.append("new");
}
String s = sb.toString();
Если можете, лучше используйте StringBuilder
или его синхронизированный брат StringBuffer
вместо String
для «интенсивной обработки строк». Он предлагает полезные методы именно для этих целей, такие как append ()
, insert ()
, delete ()
и т. Д. Также см. его javadoc .
Эффективная упаковка строк в памяти! Однажды я написал класс Set с эффективным использованием гиперпамяти, в котором строки хранились в виде дерева. Если лист был достигнут путем обхода букв, запись содержалась в наборе. Быстро работать и идеально подходит для хранения большого словаря.
И не забывайте, что строки часто являются самой большой частью памяти почти в каждом профилированном мной приложении, поэтому не обращайте на них внимания, если они вам нужны.
Иллюстрация:
У вас есть 3 струны: пиво, бобы и кровь. Вы можете создать такую древовидную структуру:
B
+-e
+-er
+-ans
+-lood
Очень эффективно, например, список названий улиц, это, очевидно, наиболее разумно с фиксированным словарем, потому что вставка не может быть выполнена эффективно. Фактически структура должна быть создана один раз, затем сериализована и после этого просто загружена.
Сначала решите, насколько пострадает ваше приложение и разработчики, если вы избавитесь от части парсинга. Более быстрое приложение не принесет вам никакой пользы, если в процессе удвоится текучесть кадров! Думаю, исходя из вашего вопроса, можно предположить, что вы уже прошли этот тест.
Во-вторых, если вы не можете отказаться от создания объекта, то вашей следующей целью должно стать обеспечение того, чтобы он не пережил Eden collection. И parse-lookup может решить эту проблему. Однако кэш, "реализованный должным образом" (я не согласен с этой базовой предпосылкой, но не буду утомлять вас сопутствующими разглагольствованиями), обычно приводит к спорам о потоках. Вы заменяете один вид давления на память другим.
Существует разновидность идиомы разбора-поиска, которая меньше страдает от побочного ущерба, который обычно возникает при полном кэшировании, и это простая предварительно вычисленная таблица поиска (см. также "мемоизация"). Шаблон, который вы обычно видите для этого, - Type Safe Enumeration (TSE). С помощью TSE вы разбираете строку, передаете ее в TSE для получения соответствующего перечислимого типа, а затем выбрасываете строку.
Текст, который вы обрабатываете, имеет свободную форму, или ввод должен соответствовать жесткой спецификации? Если большая часть вашего текста сводится к фиксированному набору возможных значений, то TSE может помочь вам здесь и послужить большему мастеру: Добавление контекста/семантики к вашей информации в момент создания, а не в момент использования.