Почему я получаю OutOfMemoryError при вставке 50 000 объектов в HashMap?

Я пытаюсь вставить около 50 000 объектов (и, следовательно, 50 000 ключей) в java.util.HashMap<java.awt.Point, Segment>. Тем не менее, я продолжаю получать исключение OutOfMemory. (Segment - мой собственный класс - очень легкий вес - одно поле String и поля 3 int).

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.HashMap.resize(HashMap.java:508)
    at java.util.HashMap.addEntry(HashMap.java:799)
    at java.util.HashMap.put(HashMap.java:431)
    at bus.tools.UpdateMap.putSegment(UpdateMap.java:168)

Это кажется довольно смешным, так как я вижу, что на машине доступно много памяти - как в свободной памяти, так и в пространстве HD для виртуальной памяти.

Возможно ли, что Java работает с некоторыми строгими требованиями к памяти? Могу ли я увеличить их?

Есть ли какое-то странное ограничение с помощью HashMap? Должен ли я реализовывать свои собственные? Есть ли другие классы, на которые стоит обратить внимание?

(Я запускаю Java 5 под OS X 10.5 на компьютере Intel с 2 ГБ оперативной памяти.)

Ответ 1

Вы можете увеличить максимальный размер кучи, передав-jmx128m (где 128 - количество мегабайт) в java. Я не помню размер по умолчанию, но мне кажется, что это было что-то довольно маленькое.

Вы можете программно проверить, сколько памяти доступно с помощью класса Runtime.

// Get current size of heap in bytes
long heapSize = Runtime.getRuntime().totalMemory();

// Get maximum size of heap in bytes. The heap cannot grow beyond this size.
// Any attempt will result in an OutOfMemoryException.
long heapMaxSize = Runtime.getRuntime().maxMemory();

// Get amount of free memory within the heap in bytes. This size will increase
// after garbage collection and decrease as new objects are created.
long heapFreeSize = Runtime.getRuntime().freeMemory();

(Пример из Альманах разработчиков Java)

Это также частично рассматривается в Часто задаваемые вопросы о виртуальной машине Java HotSpot и в Страница настройки Java 6 GC.

Ответ 2

Некоторые люди предлагают изменить параметры HashMap, чтобы ужесточить требования к памяти. Я бы предложил измерить, а не гадать; это может быть что-то другое, вызывающее OOME. В частности, я бы предложил использовать NetBeans Profiler или VisualVM (который поставляется с Java 6, но я вижу, что вы застряли с Java 5).

Ответ 3

Еще одна вещь, которую нужно попробовать, если вы знаете количество объектов заранее, - использовать конструктор HashMap (int capacity, double loadfactor) вместо стандартного no-arg, который использует значения по умолчанию (16,0.75). Если количество элементов в вашем HashMap превышает (capacity * loadfactor), то базовый массив в HashMap будет изменен до следующего значения 2, и таблица будет перефразирована. Этот массив также требует смежной области памяти, поэтому, например, если вы удваиваете размер с 32768 до массива размера 65536, вам понадобится 256kB кусок свободной памяти. Чтобы избежать дополнительного распределения и повторных штрафов, просто используйте большую хэш-таблицу с самого начала. Это также уменьшит вероятность того, что у вас не будет смежной области памяти, достаточно большой, чтобы соответствовать карте.

Ответ 4

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

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

Обычно причиной такой проблемы является код, который увеличивает емкость карты путем копирования элементов в больший массив. Существуют "немые" реализации и умные, которые используют коэффициент роста или нагрузки, который определяет размер нового массива на основе размера старого массива. Некоторые реализации скрывают эти параметры, а некоторые - не так, что вы не можете их всегда устанавливать. Проблема в том, что когда вы не можете установить его, он выбирает некоторый коэффициент загрузки по умолчанию, например 2. Таким образом, новый массив в два раза больше старого. Теперь у вашей предположительно 50k-карты есть массив подпорки 100k.

Посмотрите, можете ли вы уменьшить коэффициент нагрузки до 0,25 или что-то в этом роде. это приводит к большему количеству конфликтов хеш-карт, которые наносят ущерб производительности, но вы сталкиваетесь с узким местом памяти и должны это делать.

Используйте этот конструктор:

(http://java.sun.com/javase/6/docs/api/java/util/HashMap.html#HashMap(int, float))

Ответ 5

Вероятно, вам нужно установить флаг -Xmx512m или некоторое большее число при запуске java. Я думаю, 64mb по умолчанию.

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

Ответ 7

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

Ответ 8

По умолчанию JVM использует ограниченное пространство кучи. Предел JVM зависит от реализации, и неясно, какую JVM вы используете. В ОС, отличной от Windows, 32-разрядная Sun JVM на машине с 2 ГБ или более будет использовать максимальный размер кучи по умолчанию в 1/4 физической памяти или 512 Мб в вашем случае. Тем не менее, по умолчанию для "клиентского" режима JVM только максимальный размер кучи размером 64 МБ, что может быть тем, с чем вы столкнулись. Другие JVM-производители могут выбирать разные значения по умолчанию.

Конечно, вы можете указать ограничение кучи явно с опцией -Xmx<NN>m на java, где <NN> - это количество мегабайт для кучи.

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

Подробнее см. "Эргономика в 5.0 JVM" .

Ответ 9

Явное пространство Java ограничено по умолчанию, но это все еще звучит экстремально (хотя насколько велики ваши 50000 сегментов?)

Я подозреваю, что у вас есть еще одна проблема, например, массивы в наборе становятся слишком большими, потому что все назначается в один и тот же "слот" (также, конечно, влияет на производительность). Однако это маловероятно, если ваши точки распределены равномерно.

Мне интересно, почему вы используете HashMap, а не TreeMap? Даже если точки двухмерны, вы можете подклассифицировать их с помощью функции сравнения, а затем выполнить поиск в журналах (n).

Ответ 10

Случайная мысль: хэш-ведра, связанные с HashMap, не особенно эффективны с точки зрения памяти. Возможно, вы захотите попробовать TreeMap в качестве альтернативы и посмотреть, все еще ли она обеспечивает достаточную производительность.