Linux下Java的虚拟内存使用情况,使用的内存过多测量内存的不同方法了解虚拟内存映射虚拟内存大小何时重要?驻留集大小何时重要?底线

2022-08-31 05:14:34

我在Linux下运行的Java应用程序有问题。

当我使用默认的最大堆大小 (64 MB) 启动应用程序时,我看到使用 tops 应用程序,为应用程序分配了 240 MB 的虚拟内存。这会给计算机上的某些其他软件带来一些问题,这些软件相对资源有限。

据我所知,保留的虚拟内存无论如何都不会被使用,因为一旦我们达到堆限制,就会抛出一个。我在窗口下运行了相同的应用程序,我看到虚拟内存大小和堆大小相似。OutOfMemoryError

无论如何,我是否可以配置Linux下用于Java进程的虚拟内存?

编辑1:问题不在于堆。问题是,例如,如果我设置了128 MB的堆,Linux仍然分配了210 MB的虚拟内存,这是永远不需要的。

编辑 2:使用允许限制虚拟内存量。如果设置的大小低于 204 MB,则应用程序即使不需要 204 MB(仅 64 MB),也不会运行。所以我想了解为什么Java需要这么多虚拟内存。这可以改变吗?ulimit -v

编辑3:系统中还有其他几个应用程序正在运行,这些应用程序是嵌入式的。而且系统确实有虚拟内存限制(来自评论,重要细节)。


答案 1

这一直是Java长期以来的抱怨,但它在很大程度上毫无意义,并且通常基于查看错误的信息。通常的措辞是这样的:“Java上的Hello World需要10兆字节!为什么需要它?好吧,这里有一种方法可以使64位JVM上的Hello World声称超过4千兆字节...至少通过一种形式的测量。

java -Xms1024m -Xmx4096m com.example.Hello

测量内存的不同方法

在 Linux 上,top 命令为您提供了几个不同的内存编号。以下是它对 Hello World 示例的描述:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 2120 kgregory  20   0 4373m  15m 7152 S    0  0.2   0:00.10 java
  • VIRT是虚拟内存空间:虚拟内存映射中所有内容的总和(见下文)。它在很大程度上是没有意义的,除非它不是(见下文)。
  • RES 是驻留集大小:当前驻留在 RAM 中的页数。在几乎所有情况下,这是您在说“太大”时应该使用的唯一数字。但它仍然不是一个很好的数字,特别是在谈论Java时。
  • SHR 是与其他进程共享的驻留内存量。对于 Java 进程,这通常仅限于共享库和内存映射的 JAR 文件。在这个例子中,我只运行了一个Java进程,所以我怀疑7k是操作系统使用的库的结果。
  • 默认情况下,SWAP 未打开,此处不显示。它指示当前驻留在磁盘上的虚拟内存量,无论它实际上是否在交换空间中。操作系统非常擅长在RAM中保持活动页面,交换的唯一方法是(1)购买更多内存,或(2)减少进程数,因此最好忽略此数字。

Windows任务管理器的情况有点复杂。在Windows XP下,有“内存使用情况”和“虚拟内存大小”列,但官方文档对它们的含义保持沉默。Windows Vista和Windows 7添加了更多列,它们实际上被记录下来。其中,“工作集”测量是最有用的;它大致对应于 Linux 上 RES 和 SHR 的总和。

了解虚拟内存映射

进程消耗的虚拟内存是进程内存映射中所有内容的总和。这包括数据(例如,Java堆),还包括程序使用的所有共享库和内存映射文件。在Linux上,您可以使用pmap命令来查看映射到进程空间的所有内容(从这里开始,我只参考Linux,因为它是我使用的;我相信Windows有等效的工具)。以下是“Hello World”程序的记忆图摘录;整个内存映射的长度超过100行,并且有一千行列表并不罕见。

0000000040000000     36K r-x--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000      8K rwx--  /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000    676K rwx--    [ anon ]
00000006fae00000  21248K rwx--    [ anon ]
00000006fc2c0000  62720K rwx--    [ anon ]
0000000700000000 699072K rwx--    [ anon ]
000000072aab0000 2097152K rwx--    [ anon ]
00000007aaab0000 349504K rwx--    [ anon ]
00000007c0000000 1048576K rwx--    [ anon ]
...
00007fa1ed00d000   1652K r-xs-  /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000   1024K rwx--    [ anon ]
00007fa1ed2d3000      4K -----    [ anon ]
00007fa1ed2d4000   1024K rwx--    [ anon ]
00007fa1ed3d4000      4K -----    [ anon ]
...
00007fa1f20d3000    164K r-x--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000   1020K -----  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000     28K rwx--  /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000     16K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3837000      4K rwx--  /lib/x86_64-linux-gnu/libc-2.13.so
...

格式的快速说明:每行都以段的虚拟内存地址开头。后跟段大小、权限和段的源。最后一项是文件或“anon”,表示通过 mmap 分配的内存块。

从顶部开始,我们有

  • JVM加载器(即,键入时运行的程序)。这是非常小的;它所做的就是加载到存储真实JVM代码的共享库中。java
  • 一堆保存 Java 堆和内部数据的 anon 块。这是一个 Sun JVM,因此堆被分成多代,每一代都是它自己的内存块。请注意,JVM 根据该值分配虚拟内存空间。这允许它具有连续的堆。该值在内部用于说明程序启动时堆的“使用中”量,并在接近该限制时触发垃圾回收。-Xmx-Xms
  • 内存映射的 JARfile,在本例中是保存“JDK 类”的文件。当您对 JAR 进行内存映射时,您可以非常高效地访问其中的文件(而不是每次都从头开始读取它)。Sun JVM 将内存映射类路径上的所有 JAR;如果您的应用程序代码需要访问 JAR,您还可以对其进行内存映射。
  • 两个线程的每线程数据。1M 块是线程堆栈。我对4k块没有很好的解释,但是@ericsoe将其标识为“保护块”:它没有读/写权限,因此如果访问将导致段错误,JVM捕获并将其转换为.对于一个真正的应用程序,你会看到数十个(如果不是数百个)通过内存映射重复的条目。StackOverFlowError
  • 保存实际 JVM 代码的共享库之一。其中有几个。
  • C 标准库的共享库。这只是JVM加载的众多事情之一,这些内容并不是Java的严格组成部分。

共享库特别有趣:每个共享库至少有两个段:包含库代码的只读段,以及包含库的全局每个进程数据的读写段(我不知道没有权限的段是什么;我只在x64 Linux上见过它)。库的只读部分可以在使用该库的所有进程之间共享;例如,具有可以共享的 1.5M 虚拟内存空间。libc

虚拟内存大小何时重要?

虚拟内存映射包含很多东西。其中一些是只读的,一些是共享的,其中一些是分配的但从未接触过(例如,在这个例子中,几乎所有的4Gb堆)。但是操作系统足够智能,只能加载它需要的内容,因此虚拟内存大小在很大程度上是无关紧要的。

虚拟内存大小很重要的地方是,如果您在 32 位操作系统上运行,则只能分配 2Gb(或在某些情况下为 3Gb)的进程地址空间。在这种情况下,您正在处理稀缺的资源,并且可能必须进行权衡,例如减小堆大小以内存映射大文件或创建大量线程。

但是,鉴于64位机器无处不在,我认为虚拟内存大小成为一个完全无关紧要的统计数据不会很快。

驻留集大小何时重要?

驻留集大小是实际位于 RAM 中的虚拟内存空间部分。如果您的 RSS 逐渐成为您总物理内存的重要组成部分,那么可能是时候开始担心了。如果您的 RSS 增长到占用了您所有的物理内存,并且您的系统开始交换,那么早就该开始担心了。

但RSS也具有误导性,特别是在负载较轻的机器上。操作系统不会花费大量精力来回收进程使用的页面。这样做几乎没有什么好处,如果该过程将来触及页面,则可能会出现代价高昂的页面错误。因此,RSS 统计信息可能包括许多未在使用中的页面。

底线

除非您正在交换,否则不要过分担心各种内存统计信息告诉您的内容。需要注意的是,不断增长的RSS可能表明某种内存泄漏。

使用Java程序,关注堆中发生的事情要重要得多。消耗的空间总量很重要,您可以采取一些步骤来减少这种情况。更重要的是您在垃圾回收中花费的时间,以及堆的哪些部分正在被收集。

访问磁盘(即数据库)成本高昂,内存成本低廉。如果您可以将一个换成另一个,请这样做。


答案 2

Java和glibc有一个已知的问题>= 2.10(包括Ubuntu >= 10.04,RHEL >= 6)。

解决方法是设置这个环境。变量:

export MALLOC_ARENA_MAX=4

如果您正在运行Tomcat,则可以将其添加到文件中。TOMCAT_HOME/bin/setenv.sh

对于 Docker,将其添加到 Dockerfile

ENV MALLOC_ARENA_MAX=4

有一篇关于设置MALLOC_ARENA_MAX的 IBM 文章 https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

这篇博客文章说

已知驻留内存以类似于内存泄漏或内存碎片的方式蠕变。

还有一个开放的JDK错误JDK-8193521“glibc使用默认配置浪费内存”

在Google或SO上搜索MALLOC_ARENA_MAX以获取更多参考。

您可能还希望调整其他 malloc 选项,以针对已分配内存的低碎片进行优化:

# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536