Java 6 Чрезмерное использование памяти

Является ли Java 6 большей памятью, чем вы ожидаете для больших приложений?

У меня есть приложение, которое я разрабатывал в течение многих лет, которое до сих пор занимало около 30-40 МБ в моей конкретной тестовой конфигурации; теперь с Java 6u10 и 11 он принимает несколько сотен активных. Он отскакивает вокруг много, где-то между 50M и 200M, и когда он простаивает, он делает GC и бросает память прямо вниз. Кроме того, он генерирует миллионы ошибок страницы. Все это наблюдается через диспетчер задач Windows.

Итак, я запустил его под моим профилировщиком (jProfiler) и с помощью jVisualVM, и оба они указывают обычные привычки средней кучи и perm-gen около 30M вместе, даже когда они полностью активны в моем цикле нагрузочных тестов.

Итак, я озадачен! И это не просто запрашивает больше памяти из пула виртуальной памяти Windows - это отображается как 200M "Использование Mem".

ПОДТВЕРЖДЕНИЕ: Я хочу быть предельно ясным в этом вопросе, наблюдаемом в течение 18-часового периода с Java VisualVM, куча классов и куртка пермского поколения были абсолютно стабильными. Выделенная волатильная куча (eden и tenured) сидит без движения на 16 МБ (которая достигает в первые несколько минут), и использование этой памяти колеблется в идеальном паттерне роста равномерно с 8 МБ до 16 МБ, после чего GC запускается в возвращается к 8 МБ. За этот 18-часовой период система находилась под постоянной максимальной нагрузкой, так как я проводил стресс-тест. Это поведение прекрасно и последовательно воспроизводимо, видно по многочисленным тиражам. Единственная аномалия в том, что, хотя это происходит, память, взятая из Windows, наблюдаемая через диспетчер задач, колеблется повсеместно от 64 МБ до 900 МБ.

UPDATE 2008-12-18: Я запускаю программу с -Xms16M -Xmx16M без какого-либо очевидного неблагоприятного воздействия - производительность прекрасна, общее время работы примерно одинаковое. Но использование памяти в краткосрочной перспективе все еще достигло максимума примерно в 180 миллионов.

Обновление 2009-01-21: Кажется, ответ может быть в числе потоков - см. мой ответ ниже.


EDIT: И я имею в виду миллионы ошибок страниц буквально - в области 30M +.

EDIT: у меня есть машина 4G, поэтому 200M не имеет значения в этом отношении.

Ответ 1

В течение последних нескольких недель у меня возникла причина для исследования и исправления проблемы с объектом объединения потоков (многопоточный пул предварительной подготовки Java 6), где было запущено гораздо больше потоков, чем требуется. В заданиях может быть до 200 ненужных потоков. И потоки постоянно умирали, а новые заменяли их.

Исправив эту проблему, я решил снова запустить тест, и теперь кажется, что потребление памяти стабильно (хотя 20 или более MB выше, чем у старых JVM).

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

Если кто-то захочет экспериментировать и ответить на это своими выводами, я приму этот ответ; в противном случае я соглашусь с этим (после 2-дневного периода ожидания).

Кроме того, скорость сбоя страницы снижается (в 10 раз).

Кроме того, исправления в пуле потоков исправили некоторые проблемы, связанные с конфликтом.

Ответ 2

В ответ на обсуждение в комментариях к ответу Ran, здесь тестовый пример, который доказывает, что JVM освободит память обратно в ОС при определенных обстоятельствах:

public class FreeTest
{
    public static void main(String[] args) throws Exception
    {
        byte[][] blob = new byte[60][1024*1024];
        for(int i=0; i<blob.length; i++)
        {
            Thread.sleep(500);
            System.out.println("freeing block "+i);
            blob[i] = null;
            System.gc();
        }
    }
}

Я вижу, что размер JVM-процесса уменьшается, когда счетчик достигает около 40, как на Java 1.4, так и на Java 6 JVM (от Sun).

Вы даже можете настроить точное поведение с помощью - XX: MaxHeapFreeRatio и -XX: параметры MinHeapFreeRatio - некоторые из параметров на этой странице может также помочь ответить на исходный вопрос.

Ответ 3

Я не знаю о ошибках страниц. но об огромной памяти, выделенной для Java:

  • Sun JVM только выделяет память, никогда не освобождает ее (до смерти JVM) освобождает память только после того, как определенное соотношение между потребностями внутренней памяти и выделенной памятью падает ниже (настраиваемого) значения. JVM начинается с суммы, указанной в -Xms, и может быть увеличена до суммы, указанной в -Xmx. Я не уверен, что такое по умолчанию. Всякий раз, когда JVM требуется больше памяти (новые объекты/примитивы/массивы), он выделяет весь блок из ОС. Однако, когда потребность утихает (кратковременная потребность, см. Также 2), она не освобождает память сразу от ОС, но сохраняет ее до тех пор, пока это соотношение не будет достигнуто. Мне когда-то говорили, что JRockit ведет себя лучше, но я не могу его проверить.

  • Sun JVM запускает полный GC на основе нескольких триггеров. Один из них - объем доступной памяти - когда он слишком сильно падает, JVM пытается выполнить полный GC, чтобы освободить еще немного. Таким образом, когда больше памяти выделяется из ОС (мгновенная потребность), вероятность полного GC снижается. Это означает, что, хотя вы можете видеть 30Mb "живых" объектов, может быть намного больше "мертвых" объектов (недоступных), просто ожидая, что GC произойдет. Я знаю, что у yourkit есть отличный вид, называемый "мертвые объекты", где вы можете увидеть эти "левые".

  • В режиме "-сервера" Sun JVM запускает GC в параллельном режиме (в отличие от старого серийного "stop the world" GC). Это означает, что, хотя может быть сбор мусора, он может быть не собран сразу же из-за того, что другие потоки принимают все доступное процессорное время. Он будет собран до того, как выйдет из памяти (ну, пожалуйста, см. http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html), если из ОС можно выделить больше памяти, это может быть до запуска GC.

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

edit: изменено "никогда не отменяет" до "только после достижения соотношения".

Ответ 4

Чрезмерное создание потоков прекрасно объясняет вашу проблему:

  • Каждая нить получает свой собственный стек, который отделен от памяти кучи и, следовательно, не зарегистрирован профайлерами
  • Размер стека по умолчанию довольно большой, IIRC 256KB (по крайней мере он был для Java 1.3)
  • Стек стека протектора, вероятно, не используется повторно, поэтому, если вы создаете и уничтожаете множество потоков, вы получите множество ошибок страницы.

Если вам действительно нужно иметь сотни потоков, размер потока стека можно настроить с помощью параметра командной строки -Xss.

Ответ 5

Сбор мусора - довольно загадочная наука. По мере развития состояния, неконтролируемое поведение изменится в ответ.

Java 6 имеет разные настройки по умолчанию GC и другую "эргономику" для более ранних версий JVM. Если вы сообщите ему, что он может использовать больше памяти (либо явно в командной строке, либо неявно, не указав ничего более явного), он будет использовать больше памяти, если будет полагать, что это может повысить производительность.

В этом случае Java 6, похоже, полагает, что резервирование лишнего пространства, в которое может перерасти кучу, даст ему лучшую производительность - по-видимому, потому, что считает, что это приведет к тому, что больше объектов погибнет в пространстве Эдена и ограничит количество объектов, продвигаемых к пространству поколенного поколения. Из спецификации вашего оборудования JVM не считает, что это дополнительное зарезервированное место для кучи вызовет любые проблемы. Обратите внимание, что многие (хотя и не все) допущения, которые JVM делает в своем заключении, основаны на "типичных" приложениях, а не на вашем конкретном приложении. Он также делает предположения на основе вашего оборудования и профиля ОС.

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

Информацию об изменениях производительности в java 6 можно найти здесь.

Обсуждается управление памятью и последствиями производительности в "Белая книга управления памятью" .

Ответ 6

Много памяти выделено за пределами кучи Java после обновления до Java 6u10? Может быть только одно:

Заметки о выпуске Java6 u10: "Новый Direct3D ускоренный Rendering Pipeline (...) включен по умолчанию"

Sun включил Direct 3D ускорения по умолчанию в Java 6u10. Этот параметр создает много буферов внутренней памяти (временных?), Которые выделяются вне Java-кучи. Добавьте следующий аргумент vm, чтобы отключить его снова:

-Dsun.java2d.d3d = false

Обратите внимание, что это НЕ отключит двумерное аппаратное ускорение, а некоторые функции, которые могут использовать аппаратное ускорение 3D. Вы увидите, что ваше использование кучи Java будет увеличиваться до 7 МБ, но это хороший компромисс, потому что вы сохраните ~ 100 МБ (+) этой временной энергозависимой памяти.

Я провел много испытаний в двух настольных приложениях Swing на двух платформах:

  • high-end Intel-i7 с графической картой nVidia GTX 260,
  • трехлетний ноутбук с графикой Intel.

На обеих аппаратных платформах эта опция сделала практически нулевую субъективную разницу. (Включены тесты: прокрутка таблиц, масштабирование графических схем, диаграмм и т.д.). На нескольких тестах, где что-то было тонко отличается, отключение d3d конт-интуитивно увеличивало производительность. Я подозреваю, что проблемы управления памятью/пропускной способностью противодействовали всем преимуществам, которые должны были выполнять ускоренные функции d3d. (Ваш пробег может отличаться!)

Если вам нужно выполнить некоторую настройку производительности, отличная ссылка (например, "Устранение неполадок Java 2D" )

Ответ 7

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