Пакетное использование памяти в Java

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

Я написал небольшую программу-пример, пытаясь это продемонстрировать. Это в основном имеет 4 кнопки...

  1. Заполнить переменную области видимости класса BigList = new ArrayList<string>() с около 25 000 000 длинных предметов строки.
  2. Вызов BigList.clear()
  3. Перераспределить список - BigList = new ArrayList<string>() снова (чтобы уменьшить размер списка)
  4. Вызов System.gc() - Да, я знаю, это не значит, что GC действительно будет работать, но это то, что у нас есть.

Итак, затем я провел некоторое тестирование в Windows, Linux и Mac OS, используя мониторы задач по умолчанию, чтобы проверить процессы, о которых сообщалось, об использовании памяти. Вот что я нашел...

  • Windows - накачка списка, вызов clear и последующий вызов GC несколько раз не уменьшат использование памяти вообще. Тем не менее, перераспределение списка с помощью new и затем вызов GC несколько раз уменьшит использование памяти до начального уровня. ИМО, это приемлемо.
  • Linux (я использовал дистрибутив Mint 11 с Sun JVM) - те же результаты, что и в Windows.
  • Mac OS - я выполнил те же действия, что и выше, но даже при повторной инициализации списка вызовы GC, похоже, не имеют никакого эффекта. Программа будет сидеть, используя сотни МБ ОЗУ, хотя у меня ничего нет в памяти.

Кто-нибудь может мне это объяснить? Некоторые люди рассказали мне кое-что о "кучной" памяти, но я до сих пор не до конца ее понимаю и не уверен, что она применима здесь. Из того, что я слышал об этом, я не должен видеть поведение, которое я нахожусь в Windows и Linux в любом случае.

Это просто разница в том, как монитор активности Mac OS измеряет использование памяти, или что-то еще происходит? Я бы предпочел, чтобы моя программа не работала на холостом ходу с тоннами использования оперативной памяти Спасибо за ваше понимание.

8 ответов

Решение

Sun/Oracle JVM не возвращает ненужную память в систему. Если вы дадите ему большой максимальный размер кучи и в какой-то момент будете использовать это пространство кучи, JVM не вернет его ОС для других целей. Это будут делать другие JVM (раньше это делал JRockit, но я не думаю, что так будет).

Итак, для Oracles JVM вам нужно настроить свое приложение и свою систему на пиковую нагрузку, вот как это работает. Если используемой вами памятью можно управлять с помощью байтовых массивов (таких как работа с изображениями или чем-то еще), тогда вы можете использовать сопоставленные байтовые буферы вместо байтовых массивов Java. Отображенные байтовые буферы берутся прямо из системы и не являются частью кучи. Когда вы освобождаете эти объекты (и я считаю, что они GC'd, но не уверен), память будет возвращена в систему. Вам, вероятно, придется поиграть с этим, если предположить, что он вообще применим.

... но мне всегда кажется, что когда Java затрагивает какую-то память, она исчезает навсегда. Вы никогда не получите его обратно.

Это зависит от того, что вы подразумеваете под "навсегда".

Я также слышал, что некоторые JVM возвращают память ОС, когда они готовы и способны. К сожалению, учитывая то, как обычно работают низкоуровневые API памяти, JVM должна отдавать целые сегменты, и, как правило, сложно "эвакуировать" сегмент, чтобы его можно было вернуть.

Но я бы не стал полагаться на это... потому что есть разные вещи, которые могут помешать возвращению памяти. Скорее всего, JVM не вернет память операционной системе. Но это не "навсегда" в том смысле, что JVM будет продолжать использовать его. Даже если JVM никогда не достигнет пикового уровня использования, вся эта память поможет повысить эффективность работы сборщика мусора.

В этом случае вы должны убедиться, что ваша максимальная память никогда не будет слишком высокой, иначе ваше приложение будет постоянно поглощать сотни МБ ОЗУ.

Это неправда. Предполагая, что вы принимаете стратегию, начинающуюся с небольшой кучи и позволяющую ей расти, JVM не будет запрашивать значительно больше памяти, чем пиковая память. JVM не будет постоянно поглощать больше памяти... если ваше приложение не имеет утечки памяти и (как результат) его пиковое требование к памяти не имеет границ.

(Комментарии ОП ниже показывают, что это не то, что он пытался сказать. Тем не менее, это то, что он сказал.)


Что касается эффективности сбора мусора, мы можем смоделировать стоимость запуска эффективного сборщика мусора как:

cost ~= (amount_of_live_data * W1) + (amount_of_garbage * W2)

где W1 и W2 - (мы предполагаем) постоянные, которые зависят от коллектора. (На самом деле, это чрезмерное упрощение. Первая часть не является линейной функцией от числа живых объектов. Однако я утверждаю, что это не имеет значения для следующего.)

Эффективность коллектора может быть заявлена ​​как:

efficiency = cost / amount_of_garbage_collected

который (если мы предположим, что GC собирает все данные) расширяется до

efficiency ~= (amount_of_live_data * W1) / amount_of_garbage + W2.

Когда GC работает,

heap_size ~= amount_of_live_data + amount_of_garbage

так

efficiency ~= W1 * (amount_of_live_data / (heap_size - amount_of_live_data) )
              + W2.

Другими словами:

  • при увеличении размера кучи эффективность стремится к константе (W2), но
  • вам нужно большое соотношение heap_size и amount_of_live_data, чтобы это произошло.

Другой момент заключается в том, что для эффективного копирующего сборщика W2 покрывает только стоимость обнуления пространства, занимаемого мусорными объектами в "из космоса". Остальное (отслеживание, копирование живых объектов в "космос" и обнуление "из космоса", которое они занимали) является частью первого слагаемого исходного уравнения, т. Е. Покрывается W1. Это означает, что W2, вероятно, быть значительно меньше, чем W1 ... и что первый член окончательного уравнения имеет значение для дольше.

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

Большинство JVM не могут или не могут освободить ранее полученную память обратно в хост-ОС, если в этом нет необходимости. Это потому, что это дорогостоящая и сложная задача. Сборщик мусора применяется только к куче памяти в виртуальной машине Java. Поэтому не отдает (free() в C условия) память для ОС. Например, если большой объект больше не используется, память будет помечена как свободная в куче JVM GC и не будет передана ОС.

Распространенным заблуждением является то, что Java использует память во время работы, и там она должна иметь возможность вернуть память в ОС. На самом деле Oracle/Sun JVM резервирует виртуальную память как непрерывный блок памяти, как только она запускается. Если не хватает доступной непрерывной виртуальной памяти, она не запускается при запуске, даже если программа не будет использовать так много.

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

Как только программа завершает работу, уменьшается ли использование памяти в диспетчере задач в Windows? Я думаю, что память освобождается, но не отображается как освобожденная мониторами задач по умолчанию в ОС, которую вы отслеживаете. Пройдите этот вопрос по C++. Проблема с освобождением вектора указателей

Java выделяет память только для объектов. Там нет явного распределения памяти. Фактически Java даже рассматривает типы массивов как объекты. Каждый раз, когда объект создается, он попадает в кучу.

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

призвание System.gc() не можете собирать мусор во время звонка; вот почему ваша память не уменьшается. В общем, лучше позволить системе решить, когда ей нужно собрать кучу, и следует ли делать полный сбор.

System.gc() даже не форсирует сборку мусора; это просто намек на JVM, что "сейчас самое время немного прибраться"

Память Java объясняется здесь link2

Sun/Oracle выпустила несколько замечательных документов, описывающих сборку мусора в Java. Быстрый поиск по "Настройке сборки мусора Java" дает такие результаты, как; http://www.oracle.com/technetwork/java/gc-tuning-5-138395.html и http://java.sun.com/docs/hotspot/gc1.4.2/

Внедрение Oracle doc состояний;

Платформа Java TM 2 Standard Edition (платформа J2SE TM) используется для самых разных приложений: от небольших апплетов на настольных компьютерах до веб-сервисов на больших серверах. В платформе J2SE версии 1.4.2 было четыре сборщика мусора, из которых можно было выбирать, но без явного выбора пользователем всегда выбирался последовательный сборщик мусора. В версии 5.0 выбор коллектора зависит от класса компьютера, на котором запущено приложение.

Этот "разумный выбор" сборщика мусора, как правило, лучше, но не всегда лучший. Для пользователя, который хочет сделать свой выбор сборщиков мусора, этот документ предоставит информацию, на которой можно основывать этот выбор. Сначала это будет включать общие функции сборщиков мусора и параметры настройки, чтобы максимально эффективно использовать эти функции. Примеры приведены в контексте серийного коллекционера "Останови мир". Затем будут обсуждены особенности других сборщиков, а также факторы, которые следует учитывать при выборе одного из других сборщиков.

Они описывают различные типы доступных коллекторов и ситуации, в которых они должны использоваться. Я помню, как использовал это вместе с JConsole, чтобы отслеживать работу приложения при запуске с различными вариантами.

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

Я столкнулся с этой проблемой в Windows и нашел решение, поэтому выкладываю его как ответ на тот случай, если он может помочь другим.

Многие ответы здесь предполагают, что поведение Java является 1. хорошим и / или 2. неизбежным следствием сбора мусора. Это оба ложные.

Эта проблема:

Если вы похожи на меня и хотите написать Java для написания небольших приложений для рабочей станции или даже для запуска нескольких небольших процессов на сервере, то поведение Oracle в распределении памяти JVM делает его практически полностью бесполезным. Даже при запуске с -client каждый процесс JVM накапливает выделенную память и никогда не возвращает ее. Это поведение нельзя отключить. Как замечает OP: каждый процесс jvm держит свою неиспользуемую память неопределенно долго, даже если он никогда не будет использовать ее снова и даже тогда, когда другие процессы jvm голодают. Это необъяснимое поведение делает Oracle бесполезной реализацией для всех, кроме монолитных, сценариев с одним приложением.

Также: это НЕ следствие сбора мусора. Посмотрите.Net-приложения, которые работают в Windows, используют сборку мусора и вообще не страдают от этой проблемы.

Решение:

Решение, которое я нашел для этого, состояло в том, чтобы использовать JVM IKVM.NET, который вы используете как замену java.exe в Windows. Он компилирует байт-код Java в код.Net IL и работает как процесс.Net. Он также содержит утилиты для преобразования файлов.jar в сборки.Net.dll и.exe. Производительность часто лучше, чем у Oracle JVM, и после GC память немедленно возвращается в ОС. (Примечание: это также работает в Linux с Mono)

To be clear, I still rely on Oracle's JVM for all but my small applications and also to debug my small applications, but once stable, I use ikvm to run them as if they were native windows applications and this works so well, I've been amazed. It has numerous beneficial side effects. Once compiled, DLLs shared between processes are loaded only once, and applications show up in the task manager as.exe instead of all showing as javaw.exe.

Unfortunately, not everyone can use ikvm to solve this problem, but I hope this helps those in my situation.

Другие вопросы по тегам