Лучшая альтернатива для реализации String flyweight в Java - PullRequest
9 голосов
/ 26 мая 2010

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

Я посмотрел на Java Constant Pool и String.intern, но похоже, что это может спровоцировать некоторые проблемы PermGen.

Что было бы наилучшей альтернативой для реализации многопоточного пула строк в Java для всего приложения?

РЕДАКТИРОВАТЬ: См. Также мой предыдущий, связанный вопрос: Как Java реализует шаблон веса для строки под капотом?

Ответы [ 5 ]

8 голосов
/ 26 мая 2010

Примечание. В этом ответе используются примеры, которые могут не соответствовать современным библиотекам 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) );

Это целая строка теста, потому что средство сравнения просто переносит новый экземпляр Stringвокруг одних и тех же символьных данных.Сравните результаты при замене String id = matcher.group(1); на String id = new String(matcher.group(1));.

3 голосов
/ 26 мая 2010

Это уже сделано на уровне JVM. Вам нужно только убедиться, что вы не создаете new String s каждый раз, явно или неявно.

т.е. не делай:

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";
}

+= неявно создает new String в куче каждый раз. Скорее сделайте это

StringBuilder sb = new StringBuilder();
for (/* some loop condition */) {
    sb.append("new");
}
String s = sb.toString();

Если вы можете, лучше использовать StringBuilder или его синхронизированный брат StringBuffer вместо String для "интенсивной обработки строк". Он предлагает полезные методы именно для этих целей, такие как append(), insert(), delete() и т. Д.

1 голос
/ 30 мая 2015

Java 7/8

Если вы делаете то, что говорит принятый ответ, и используете Java 7 или новее, вы не делаете то, что он говорит.

Реализация subString() изменилась.

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

1950    public String substring(int beginIndex, int endIndex) {
1951        if (beginIndex < 0) {
1952            throw new StringIndexOutOfBoundsException(beginIndex);
1953        }
1954        if (endIndex > count) {
1955            throw new StringIndexOutOfBoundsException(endIndex);
1956        }
1957        if (beginIndex > endIndex) {
1958            throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
1959        }
1960        return ((beginIndex == 0) && (endIndex == count)) ? this :
1961            new String(offset + beginIndex, endIndex - beginIndex, value);
1962    }

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

1 голос
/ 27 мая 2010

Эффективно упаковывайте Строки в память! Однажды я написал класс Set, эффективный для гиперпамяти, где строки хранятся в виде дерева. Если лист был достигнут путем обхода букв, запись содержалась в наборе. С ним тоже можно работать, и он идеально подходит для хранения большого словаря.

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

Иллюстрация:

У вас есть 3 строки: пиво, бобы и кровь. Вы можете создать древовидную структуру следующим образом:

B
+-e
  +-er
  +-ans
+-lood

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

0 голосов
/ 26 мая 2010

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

Во-вторых, если вы не можете устранить создание объекта, то ваша следующая цель должна состоять в том, чтобы он не пережил коллекцию Eden. И синтаксический анализ может решить эту проблему. Тем не менее, кэш, «реализованный правильно» (я не согласен с этой основной предпосылкой, но я не буду утомлять вас сопутствующими рассуждениями), обычно приводит к конфликту потоков. Вы бы заменили один тип давления памяти на другой.

Существует вариация идиомы поиска-разбора, которая меньше страдает от вида сопутствующего ущерба, который вы обычно получаете от полного кэширования, и это простая предварительно рассчитанная таблица поиска (см. Также «памятка»). Шаблон, который вы обычно видите для этого, - Тип Safe Enumeration (TSE). С помощью TSE вы анализируете строку, передаете ее в TSE, чтобы получить связанный перечислимый тип, а затем выбрасываете строку.

Является ли текст, который вы обрабатываете, произвольной формой, или ввод должен соответствовать жесткой спецификации? Если большая часть вашего текста отрисовывается до фиксированного набора возможных значений, то TSE может помочь вам в этом и послужит более серьезному мастеру: добавление контекста / семантики к вашей информации в момент создания, а не в момент использования ,

...