Java native thread memory

How Much Memory Does a Java Thread Take?

Join the DZone community and get the full member experience.

JVM does not aggressively size committed memory to the number of threads * 1MB, that’s a wrong assumption based on the wrong NMT reporting where in Java 8 committed memory is automatically set to reserved memory. See https://bugs.openjdk.java.net/browse/JDK-8191369

The committed memory is defined by the stack depth. Thanks to Thomas Stuefe to point to this fact in comments.

A memory, which is taken by all Java threads, is a significant part of the total memory consumption of your application. There are a few techniques on how to limit the number of created threads, depending on whether your application is CPU-bound or IO-bound. If your application is rather IO-bound, you will very likely need to create a thread pool with a significant number of threads which can be bound to some IO operations (in blocked/waiting state, reading from DB, sending HTTP request).

However, if your app rather spends time on some computing task, you can, for instance, use HTTP server (e.g. Netty) with a lower number of threads and save a lot of memory. Let’s look at an example of how much memory we need to sacrifice to create a new thread.

Thread memory contains stack frames, local variables, method parameters, . and a thread size can is configured with defaults this way (in kilobytes):

$ java -XX:+PrintFlagsFinal -version | grep ThreadStackSize intx CompilerThreadStackSize = 1024 intx ThreadStackSize = 1024 intx VMThreadStackSize = 1024

Thread Memory Consumption on Java 8

$ java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary / -XX:+PrintNMTStatistics -version openjdk version "1.8.0_212" OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_212-b03) OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.212-b03, mixed mode) Native Memory Tracking: Total: reserved=6621614KB, committed=545166KB - Java Heap (reserved=5079040KB, committed=317440KB) (mmap: reserved=5079040KB, committed=317440KB) - Class (reserved=1066074KB, committed=13786KB) (classes #345) (malloc=9306KB #126) (mmap: reserved=1056768KB, committed=4480KB) - Thread (reserved=19553KB, committed=19553KB) (thread #19) (stack: reserved=19472KB, committed=19472KB) (malloc=59KB #105) (arena=22KB #34)

We can see two types of memory:

  • Reserved — the size which is guaranteed to be available by a host’s OS (but still not allocated and cannot be accessed by JVM) — it’s just a promise
  • Committed — already taken, accessible, and allocated by JVM

In a section Thread, we can spot the same number in Reserved and Committed memory, which is very close to a number of threads * 1MB. The reason is that JVM aggressively allocates the maximum available memory for threads from the very beginning.

Thread Memory Consumption on Java 11

$ java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary / -XX:+PrintNMTStatistics -version openjdk version "11.0.2" 2019-01-15 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.2+9) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.2+9, mixed mode) Native Memory Tracking: Total: reserved=6643041KB, committed=397465KB - Java Heap (reserved=5079040KB, committed=317440KB) (mmap: reserved=5079040KB, committed=317440KB) - Class (reserved=1056864KB, committed=4576KB) (classes #426) ( instance classes #364, array classes #62) (malloc=96KB #455) (mmap: reserved=1056768KB, committed=4480KB) ( Metadata: ) ( reserved=8192KB, committed=4096KB) ( used=2849KB) ( free=1247KB) ( waste=0KB =0,00%) ( Class space:) ( reserved=1048576KB, committed=384KB) ( used=270KB) ( free=114KB) ( waste=0KB =0,00%) - Thread (reserved=15461KB, committed=613KB) (thread #15) (stack: reserved=15392KB, committed=544KB) (malloc=52KB #84) (arena=18KB #28)

You may notice that we are saving a lot of memory just because we are using Java 11, which no longer aggressively allocates up to Reserved Memory at the time of thread creation. Of course, this is just java -version command, but if you try it out, you will definitely notice a big improvement.

Читайте также:  Css text wrap none

Thank you for reading my article and please let some comments below. If you like being notified about new posts, then start following me on Twitter: @p_bouda.

Opinions expressed by DZone contributors are their own.

Источник

Распределение памяти в JVM

Всем привет! Перевод сегодняшнего материала мы хотим приурочить к запуску нового потока по курсу «Разработчик Java», который стартует уже завтра. Что ж начнём.

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

JVM разделяет память на две основные категории: «кучу» (heap) и «не кучу» (non-heap). Куча — это часть памяти JVM, с которой разработчики наиболее знакомы. Здесь хранятся объекты, созданные приложением. Они остаются там до тех пор, пока не будут убраны сборщиком мусора. Как правило, размер кучи, которую использует приложение, изменяется в зависимости от текущей нагрузки.

Память вне кучи делится на несколько областей. В HotSpot для изучения областей этой памяти можно использовать механизм Native memory tracking (NMT). Обратите внимание, что, хотя NMT не отслеживает использование всей нативной памяти (например, не отслеживается выделение нативной памяти сторонним кодом), его возможностей достаточно для большинства типичных приложений на Spring. Для использования NMT запустите приложение с параметром -XX:NativeMemoryTracking=summary и с помощью jcmd VM.native_memory summary посмотрите информацию об используемой памяти.

Давайте посмотрим использование NMT на примере нашего старого друга Petclinic. Диаграмма ниже показывает использование памяти JVM по данным NMT (за вычетом собственного оверхеда NMT) при запуске Petclinic с максимальным размером кучи 48 МБ ( -Xmx48M ):

Читайте также:  Google viewer pdf html

Как вы видите, на память вне кучи приходится большая часть используемой памяти JVM, причем память кучи составляет только одну шестую часть от общего объёма. В этом случае это примерно 44 МБ (из которых 33 МБ использовалось сразу после сборки мусора). Использование памяти вне кучи составило в сумме 223 МБ.

Области нативной памяти

Compressed class space (область сжатых указателей): используется для хранения информации о загруженных классах. Ограничивается параметром MaxMetaspaceSize . Функция количества классов, которые были загружены.

Примечание переводчика

Почему-то автор пишет про «Compressed class space», а не про всю область «Class». Область «Compressed class space» входит в состав области «Сlass», а параметр MaxMetaspaceSize ограничивает размер всей области «Class», а не только «Compressed class space». Для ограничения «Compressed class space» используется параметр CompressedClassSpaceSize .

Отсюда:
If UseCompressedOops is turned on and UseCompressedClassesPointers is used, then two logically different areas of native memory are used for class metadata…
A region is allocated for these compressed class pointers (the 32-bit offsets). The size of the region can be set with CompressedClassSpaceSize and is 1 gigabyte (GB) by default…
The MaxMetaspaceSize applies to the sum of the committed compressed class space and the space for the other class metadata

Если включен параметр UseCompressedOops и используется UseCompressedClassesPointers , тогда для метаданных классов используется две логически разные области нативной памяти…

Для сжатых указателей выделяется область памяти (32-битные смещения). Размер этой области может быть установлен CompressedClassSpaceSize и по умолчанию он 1 ГБ…
Параметр MaxMetaspaceSize относится к сумме области сжатых указателей и области для других метаданных класса.

  • Thread (потоки): память, используемая потоками в JVM. Функция количества запущенных потоков.
  • Code cache (кэш кода): память, используемая JIT для его работы. Функция количества классов, которые были загружены. Ограничивается параметром ReservedCodeCacheSize . Можно уменьшить настройкой JIT, например, отключив многоуровневую компиляцию (tiered compilation).
  • GC (сборщик мусора): хранит данные, используемые сборщиком мусора. Зависит от используемого сборщика мусора.
  • Symbol (символы): хранит такие символы, как имена полей, сигнатуры методов и интернированные строки. Чрезмерное использование памяти символов может указывать на то, что строки слишком интернированы.
  • Internal (внутренние данные): хранит прочие внутренние данные, которые не входят ни в одну из других областей.

По сравнению с кучей, память вне кучи меньше изменяется под нагрузкой. Как только приложение загрузит все классы, которые будут использоваться и JIT полностью прогреется, всё перейдет в устойчивое состояние. Чтобы увидеть уменьшение использования области Compressed class space, загрузчик классов, который загрузил классы, должен быть удален сборщиком мусора. Это было распространено в прошлом, когда приложения развертывались в контейнерах сервлетов или серверах приложений (загрузчик классов приложения удалялся сборщиком мусора, когда приложение удалялось с сервера приложений), но с современными подходами к развертыванию приложений это случается редко.

Читайте также:  Functions With Arrays

Настроить JVM для эффективного использования доступной оперативной памяти непросто. Если вы запустите JVM с параметром -Xmx16M и ожидаете, что будет использоваться не более 16 МБ памяти, то вас ждёт неприятный сюрприз.

Интересной областью памяти JVM является кэш кода JIT. По умолчанию HotSpot JVM будет использовать до 240 МБ. Если кэш кода слишком мал, в JIT может не хватить места для хранения своих данных, и в результате будет снижена производительность. Если кэш слишком велик, то память может быть потрачена впустую. При определении размера кэша важно учитывать его влияние как на использование памяти, так и на производительность.

При работе в контейнере Docker последние версии Java теперь знают об ограничениях памяти контейнера и пытаются соответствующим образом изменить размер памяти JVM. К сожалению, часто происходит выделение большого количества памяти вне кучи и недостаточного в куче. Допустим, у вас есть приложение, работающее в контейнере с 2-мя процессорами и 512 МБ доступной памяти. Вы хотите, чтобы обрабатывалось больше нагрузки и увеличиваете количество процессоров до 4-х и память до 1 ГБ. Как мы обсуждали выше, размер кучи обычно изменяется в зависимости от нагрузки, а память вне кучи изменяется значительно меньше. Поэтому мы ожидаем, что большая часть дополнительных 512 МБ будет предоставлена куче, чтобы справиться с увеличенной нагрузкой. К сожалению, по умолчанию JVM этого не сделает и распределит дополнительную память более менее равномерно между памятью в куче и вне кучи.

К счастью, команда CloudFoundry обладает обширными знаниями о распределении памяти в JVM. Если вы загружаете приложения в CloudFoundry, то сборщик (build pack) автоматически применит эти знания для вас. Если вы не используете CloudFoudry или хотели бы больше понять о том, как настроить JVM, то рекомендуется прочитать описание третьей версии Java buildpack’s memory calculator.

Что это значит для Spring

Команда Spring проводит много времени, думая о производительности и использовании памяти, рассматривая возможность использования памяти как в куче, так и вне кучи. Один из способов ограничить использование памяти вне кучи — это делать части фреймворка максимально универсальными. Примером этого является использование Reflection для создания и внедрения зависимостей в бины вашего приложения. Благодаря использованию Reflection количество кода фреймворка, который вы используете, остается постоянным, независимо от количества бинов в вашем приложении. Для оптимизации времени запуска мы используем кэш в куче, очищая этот кэш после завершения запуска. Память кучи может быть легко очищена сборщиком мусора, чтобы предоставить больше доступной памяти вашему приложению.

Традиционно ждём ваши комментарии по материалу.

Источник

Оцените статью