Оптимизировать использование памяти коллекцией строк в Java - PullRequest
9 голосов
/ 07 апреля 2009

У меня есть большое количество пар имя-значение (около 100 тыс.), Которые мне нужно хранить в каком-то кеше (скажем, хэш-карте), где значение представляет собой строку со средним размером около 30 тыс. Байтов. 1001 *

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

Любые рекомендации о том, как я мог решить эту проблему?

Ответы [ 7 ]

10 голосов
/ 07 апреля 2009

Делать не использовать String.intern (с этим были разные проблемы с памятью на протяжении многих лет). вместо этого создайте свой собственный кеш, похожий на String.intern. в основном, вы хотите карту, где каждый ключ отображается на себя. затем, перед кэшированием какой-либо строки, вы «интернируете» ее:

private Map<String,WeakReference<String>> myInternMap = new WeakHashMap<String,,WeakReference<String>>();
public String intern(String value) {
  synchronized(myInternMap) {
    WeakReference<String> curRef = myInternMap.get(value);
    String curValue = ((curRef != null) ? curRef.get() : null);
    if(curValue != null) {
      return curValue;
    }

    myInternMap.put(value, new WeakReference<String>(value));
    return value;
  }
}

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

9 голосов
/ 07 апреля 2009

String.intern () поможет вам здесь (скорее всего). Это разрешит несколько экземпляров одной и той же строки до одной копии.

РЕДАКТИРОВАТЬ: Я предположил, что это "скорее всего" поможет. В каких случаях это не так? Внутренние строки будут иметь эффект постоянного хранения этих встроенных строковых представлений . Если проблемный домен является однократным процессом, это может не быть проблемой. Если это длительный процесс (например, веб-приложение), у вас вполне может быть проблема.

Я бы не решался сказать никогда использовать интернирование (я бы сказал, что никогда ничего не делать) Однако есть сценарии, где это не идеально.

4 голосов
/ 07 апреля 2009

String.intern - очевидный выбор, как говорит Брайан. Но если вы не хотите проходить через всю строку в памяти, вы можете использовать Set, чтобы сначала увидеть, присутствует ли значение. Вот непроверенный код. Вам придется потрудиться, удалив с обратной карты при удалении с основной

  class Map2<K, V> implements Map<K, V>
  {
    Map<K, V> _map = Maps.newHashMap();
    Set<V, V> _rev = Maps.newHashMap();

    V put(K k, V v) {
      if (_rev.containsKey(v)) {
        V prev = _rev.get(v);
        return _map.put(k, prev);
      } else {
        _rev.put(v, v);
        return _map.put(k,v);
      }
   }
1 голос
/ 07 апреля 2009

Договорились с другими о том, что не следует использовать String.intern (): как только вы поместите туда строку, она никогда не исчезнет. Посмотрите на ранние версии Xerces, почему это плохая идея.

Лучшим решением является использование WeakHashMap, заключая значение в WeakReference:

private Map<String,WeakReference<String>> _map 
    = new WeakHashMap<String,WeakReference<String>>();

public synchronized String intern(String str)
{
    WeakReference<String> ref = _map.get(str);
    String s2 = (ref != null) ? ref.get() : null;
    if (s2 != null)
        return s2;
    str = new String(str);
    _map.put(str, new WeakReference(str));
    return str;
}

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

РЕДАКТИРОВАТЬ: необходимо создать новую строку здесь (и я обновлю статью), потому что оригинал может быть подстрокой из гораздо большего массива символов. Я думал, что это было исправлено в JDK 1.3, но, видимо, нет (по крайней мере, в 1.5).

1 голос
/ 07 апреля 2009

Это зависит от того, как вы создаете String.

Один из возможных способов - использовать TreeSet, который использует Comparator, который может сравнивать существующие String s и источник вашего нового String. Используйте SortedSet.tailSet и Iterator, чтобы найти существующий String. Или альтернативно NavigableSet.ceiling/floor или TreeMap с аналогичной настройкой.

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

String.intern имеет проблемы с производительностью.

0 голосов
/ 08 апреля 2009

Вам действительно нужно Strings , или вам просто нужна какая-нибудь старая CharSequence ? Если нет, то подумайте о реализации "компактной" CharSequence , такой как та, которую я предлагаю в ссылке.

0 голосов
/ 08 апреля 2009

Вы можете сжать строки. Строка 30К должна получить хорошую степень сжатия. Я написал хак для сжатия большой строки в качестве упражнения, но вы можете использовать байт [] сжатых данных для хранения строки.

Строка символов 30 КБ будет использовать около 60 КБ (2 байта на символ), поэтому даже использование getBytes () может быть улучшением.

...