Будет ли OpenJDK JVM когда-либо давать кучу памяти обратно в Linux?
У нас есть долгоживущий серверный процесс, который редко требуется много оперативной памяти в течение короткого времени. Мы видим, что, как только JVM получит память из ОС, это
никогда не возвращает его обратно в ОС. Как мы попросим JVM вернуть память кучи обратно в ОС?
Как правило, принятый ответ на такие вопросы заключается в использовании
-XX:MaxHeapFreeRatio
и -XX:MinHeapFreeRatio
. (См.
1, 2, 3, 4). Но мы запускаем java следующим образом:
java -Xmx4G -XX:MaxHeapFreeRatio=50 -XX:MinHeapFreeRatio=30 MemoryUsage
и все еще видите это в VisualVM:
![Использование виртуальной памяти VM]()
Ясно, что JVM не чтит -XX:MaxHeapFreeRatio=50
, поскольку heapFreeRatio очень близок к 100% и нигде около 50%. Никакое количество нажатий на "Выполнить GC" не возвращает память в ОС.
MemoryUsage.java:
import java.util.ArrayList;
import java.util.List;
public class MemoryUsage {
public static void main(String[] args) throws InterruptedException {
System.out.println("Sleeping before allocating memory");
Thread.sleep(10*1000);
System.out.println("Allocating/growing memory");
List<Long> list = new ArrayList<>();
// Experimentally determined factor. This gives approximately 1750 MB
// memory in our installation.
long realGrowN = 166608000; //
for (int i = 0 ; i < realGrowN ; i++) {
list.add(23L);
}
System.out.println("Memory allocated/grown - sleeping before Garbage collecting");
Thread.sleep(10*1000);
list = null;
System.gc();
System.out.println("Garbage collected - sleeping forever");
while (true) {
Thread.sleep(1*1000);
}
}
}
Версии:
> java -version
openjdk version "1.8.0_66-internal"
OpenJDK Runtime Environment (build 1.8.0_66-internal-b01)
OpenJDK 64-Bit Server VM (build 25.66-b01, mixed mode)
> uname -a
Linux londo 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1+deb8u5 (2015-10-09) x86_64 GNU/Linux
> lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 8.2 (jessie)
Release: 8.2
Codename: jessie
Я также пробовал OpenJDK 1.7 и Sun Java 1.8. Все ведут себя одинаково, и никто
вернуть память в ОС.
Мне кажется, что мне нужно это, и этот обмен и пейджинг не будут "решать" это, потому что тратить диск IO на пейджинг, близкий к мусорному ядру 2 ГБ, это просто пустая трата ресурсов. Если вы не согласны, пожалуйста, просветите меня.
Я также написал немного memoryUsage.c с malloc()
/free()
, и он возвращает память в ОС. Так можно в C. Возможно, не с Java?
Изменить: Августо указал, что поиск привел бы меня к -XX:MaxHeapFreeRatio
и -XX:MinHeapFreeRatio
, работающему только с -XX:+UseSerialGC
. Я был в восторге и испробовал это, озадаченный тем, что сам этого не нашел. Да, он работал с моим MemoryUsage.java:
![-XX:+UseSerialGC working with simple app]()
Однако, когда я пробовал -XX:+UseSerialGC
с нашим реальным приложением, не так много:
![-XX:+UseSerialGC not working with real app]()
Я обнаружил, что gc() через некоторое время помог, поэтому я сделал поток, который сделал больше или меньше:
while (idle() && memoryTooLarge() && ! tooManyAttemptsYet()) {
Thread.sleep(10*1000);
System.gc();
}
и это сделало трюк:
![GC thread working]()
Ранее я видел поведение с -XX:+UseSerialGC
и несколькими вызовами System.gc()
в некоторых из моих многочисленных экспериментов, но мне не нравилась необходимость в потоке GC. И кто знает, продолжит ли это работать, как развиваются наше приложение, так и java. Должен быть лучший способ.
Какова логика, которая заставляет меня звонить System.gc()
четыре раза (но не сразу), и где этот материал задокументирован?
В поисках документации для -XX:MaxHeapFreeRatio
и -XX:MinHeapFreeRatio
, работающей только с -XX:+UseSerialGC
, я читаю документацию для java-инструмента/исполняемого файла и нигде не упоминается, что -XX:MaxHeapFreeRatio
и -XX:MinHeapFreeRatio
работает только с -XX:+UseSerialGC
. Фактически, исправленная проблема [JDK-8028391] Сделать флаги Min/MaxHeapFreeRatio управляемыми говорит:
Чтобы разрешить приложениям контролировать, как и когда разрешать более или менее GC, флаги -XX: MinHeapFreeRatio и -XX: MaxHeapFreeRatio должны быть сделаны управляемый. Поддержка этих флагов также должна быть реализована в параллельный коллектор по умолчанию.
A comment для исправленной проблемы говорит:
Поддержка этих флагов также добавлена в ParallelGC как часть политики адаптивного размера.
Я проверил, и патч, на который ссылается исправленная проблема, обращенная к openjdk-8, действительно содержится в исходный пакет tarball для версии openjdk-8, которую я использую. Поэтому он должен, по-видимому, работать в "параллельном сборщике по умолчанию", но не так, как я продемонстрировал в этом посте. Я еще не нашел документацию, в которой говорится, что она должна работать только с -XX:+UseSerialGC
. И поскольку я документировал здесь, даже это ненадежно /dicey.
Не могу ли я получить -XX:MaxHeapFreeRatio
и -XX:MinHeapFreeRatio
, чтобы выполнить то, что они обещают, не пройдя все эти обручи?
Ответы
Ответ 1
"G1 (-XX: + UseG1GC), Parallel scavenge (-XX: + UseParallelGC) и ParallelOld (-XX: + UseParallelOldGC) возвращают память, когда куча сжимается. Я не уверен в Serial и CMS, они не сокращали свою кучу в моих экспериментах.
Оба параллельных коллектора требуют нескольких GC, прежде чем уменьшать кучу до "приемлемого" размера. Это за дизайн. Они намеренно держатся за кучу, полагая, что это будет необходимо в будущем. Установка флага -XX: GCTimeRatio = 1 несколько улучшит ситуацию, но все равно потребуется несколько GC для сжатия.
G1 замечательно хорош в сжатии кучи быстро, поэтому для описанной выше утилиты я бы сказал, что она разрешима с помощью G1 и запускает System.gc()
после освобождения всех кэшей и загрузчиков классов и т.д. "
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6498735
Ответ 2
Если есть способ (скажем, m1()), который делает тяжелую работу в вашем приложении, вы можете попробовать следующее:
- вместо вызова m1(),
serialize
всех необходимых объектов
для работы метода.
- создайте основной метод, который
de-serialize
объекты, которые вы сериализовали на предыдущем шаге. и вызывает m1() - этот главный метод является точкой входа для программы P1.
- По завершении m1() сериализуйте вывод для основной программы для де-сериализации.
- выполнить P1 как отдельную программу с помощью
Runtime or ProcessBuilder
.
Таким образом, когда метод тяжелой работы m1() завершается, процесс P1 завершится, что должно освободить кучу на этой JVM.
Ответ 3
Ваша проблема - сложная задача - я бы предложил решение легко. Из того, что я прочитал, вы знаете, как кодировать как на C, так и на Java. Может быть использование JNA (Java Native Access) или JNI (Java Native Interface) может разрешить корень проблемы (кодирование тяжелой части обработки на C и вызов ее из java). Другим способом было бы сделать еще одну небольшую программу, написанную на C, сделать тяжелую работу - вы бы назвали эту небольшую программу из своего java-кода (предпочтительно в своем потоке).
Родственный доступ Java
Явный интерфейс Java