ChatGPT解决这个技术问题 Extra ChatGPT

Linux下Java的虚拟内存使用情况,使用的内存过多

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

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

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

无论如何,我可以为 Linux 下的 Java 进程配置正在使用的虚拟内存吗?

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

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

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

你为什么关心虚拟内存的使用?如果您真的想担心,请查看常驻内存使用情况并阅读以下命令:free、ps、top。
系统中运行着其他几个应用程序,这些应用程序是嵌入式的。而且系统确实有虚拟内存限制。
啊哈,魔鬼在细节中
您使用的是哪种 Java 实现。 IIRC,沼泽标准(非 OpenJDK)免费 Sun JRE 未获得嵌入式使用许可。
我想我错过了“嵌入式”部分……它是内存有限的,硬件是定制的,但它仍然是一台标准计算机

k
kdgregory

这一直是对 Java 的长期抱怨,但它在很大程度上毫无意义,并且通常基于查看错误的信息。通常的措辞类似于“Java 上的 Hello World 需要 10 兆字节!为什么需要它?”好吧,这是一种让 64 位 JVM 上的 Hello World 声称占用 4 GB 的方法……至少通过一种测量形式。

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 下,有“内存使用”和“虚拟内存大小”列,但 official documentation 没有说明它们的含义。 Windows Vista 和 Windows 7 添加了更多列,它们实际上是 documented。其中,“工作集”测量是最有用的;它大致对应于 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 加载程序(即,当您键入 java 时运行的程序)。这是非常小的;它所做的只是加载到存储真实 JVM 代码的共享库中。

一堆保存 Java 堆和内部数据的匿名块。这是一个 Sun JVM,所以堆被分成多代,每一代都是它自己的内存块。注意JVM根据-Xmx值分配虚拟内存空间;这允许它有一个连续的堆。 -Xms 值在内部用于说明程序启动时有多少堆“正在使用”,并在接近该限制时触发垃圾收集。

内存映射的 JAR 文件,在本例中是保存“JDK 类”的文件。当您对 JAR 进行内存映射时,您可以非常有效地访问其中的文件(而不是每次从头开始读取)。 Sun JVM 将对类路径上的所有 JAR 进行内存映射;如果您的应用程序代码需要访问 JAR,您还可以对其进行内存映射。

两个线程的每线程数据。 1M 块是线程栈。我对 4k 块没有很好的解释,但@ericsoe 将其识别为“保护块”:它没有读/写权限,因此如果访问会导致段错误,JVM 会捕获并翻译它到 StackOverFlowError。对于一个真正的应用程序,你会看到数十个甚至数百个这样的条目在内存映射中重复出现。

保存实际 JVM 代码的共享库之一。其中有几个。

C 标准库的共享库。这只是 JVM 加载的许多不属于 Java 的内容之一。

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

虚拟内存大小何时重要?

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

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

但是,鉴于 64 位机器无处不在,我认为用不了多久虚拟内存大小就会成为一个完全不相关的统计数据。

居民集大小何时重要?

驻留集大小是实际在 RAM 中的那部分虚拟内存空间。如果您的 RSS 增长到您的总物理内存的很大一部分,那么可能是时候开始担心了。如果您的 RSS 增长到占用您所有的物理内存,并且您的系统开始交换,那么现在就该开始担心了。

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

底线

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

对于 Java 程序,关注堆中发生的事情要重要得多。占用的空间总量很重要,您可以采取一些步骤来减少它。更重要的是您在垃圾收集上花费的时间,以及堆的哪些部分被收集。

访问磁盘(即数据库)很昂贵,而内存很便宜。如果您可以用一个换另一个,那么就这样做。


您应该考虑到 RES 度量中缺少当前换出的内存部分。因此,您的 RES 值可能较低,但这仅仅是因为应用程序处于非活动状态并且大部分堆已换出到磁盘。 Java 在交换方面做得非常糟糕:在每次完整的 GC 上,大部分堆都被遍历和复制,所以如果你的大部分堆处于交换状态,GC 必须将它们全部加载回主内存。
很好的答案kdgregory!我正在使用没有交换空间的 CF 在嵌入式环境中运行。因此,根据您的回答,我所有的 VIRT、SWAP 和 nFLT 值都来自内存映射文件……这对喵喵来说很有意义。您知道 SWAP 值是表示尚未加载到内存中的页面还是已换出内存的页面,还是两者兼而有之?我们如何才能了解可能的颠簸(连续映射然后换出)?
@Jeach - 我很惊讶报告了任何交换,因此启动了我的“旅行 Linux”(带有 Ubuntu 10.04 且没有交换的拇指驱动器)。当我在顶部启用“SWAP”列时,我看到 Eclipse 有 509m。然后我用pmap查看时,总虚拟空间为650m。所以我怀疑“SWAP”数字代表所有磁盘页面,而不仅仅是那些不在内存中的页面。
至于您的第二个问题:如果您经常从闪存卡中读取页面,那么您的 IO 等待时间(在 top 的摘要中显示为“%wa”)应该很长。但是请注意,对于任何活动,尤其是写入(假设您的程序执行任何操作),这都会很高。
> 1M块为线程栈;我不知道 4K 块中有什么。 4K 块 - 被标记为既没有读取权限也没有写入权限 - 可能是一个保护块。在堆栈溢出时,会访问该区域,这会触发故障,然后 JVM 可以通过生成 Java StackOverflowException 来处理该故障。这比在每个方法调用时检查堆栈指针要便宜得多。在其他情况下也可以看到没有设置权限的警戒区域。
L
Lari Hotari

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 https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en 的 IBM 文章

This blog post says

众所周知,常驻内存会以类似于内存泄漏或内存碎片的方式蠕动。

还有一个开放的 JDK 错误 JDK-8193521 "glibc wastes memory with default configuration"

在 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

这个答案确实帮助了我在 64 位 Ubuntu 服务器上使用 TomEE 服务器,该服务器有点“消耗内存”。 IBM 文章的链接确实是一个深刻的解释。再次感谢这个好提示!
JVM 可能会泄漏本机内存,从而导致类似的症状。请参阅stackoverflow.com/a/35610063/166062。未关闭的 GZIPInputStream 和 GZIPOutputStream 实例也可能是泄漏的来源。
Java 8 中有一个 JVM 错误,它会导致本机内存无限增长:bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8164293 - 如果这对您有影响,使用 MALLOC_ARENA_MAX 可能会减慢您的内存增长,但不能完全解决问题。
@LariHotari 非常感谢您指出 glibc 和 redhat 版本的努力
Java 8u131 包含针对相关 JVM 错误 JDK-8164293 bugs.openjdk.java.net/browse/JDK-8178124 的反向移植错误修复。
J
James Schek

为 Java 进程分配的内存量与我的预期相当。我在嵌入式/内存有限的系统上运行 Java 时遇到了类似的问题。运行具有任意 VM 限制或在没有足够交换量的系统上运行的任何应用程序往往会中断。这似乎是许多现代应用程序的本质,它们不是为在资源有限的系统上使用而设计的。

您还有更多选择可以尝试限制 JVM 的内存占用。这可能会减少虚拟内存占用:

-XX:ReservedCodeCacheSize=32m 保留代码缓存大小(以字节为单位)- 最大代码缓存大小。 [Solaris 64 位、amd64 和 -server x86:48m;在 1.5.0_06 及更早版本中,Solaris 64 位和 and64:1024m。] -XX:MaxPermSize=64m 永久代的大小。 [5.0 及更高版本:64 位 VM 的规模扩大了 30%; 1.4 amd64:96m; 1.3.1-客户端:32m。]

此外,您还应该将 -Xmx(最大堆大小)设置为尽可能接近应用程序实际峰值内存使用量的值。我相信 JVM 的默认行为仍然是每次将堆大小扩大到最大值时翻倍。如果您从 32M 堆开始并且您的应用程序达到 65M 的峰值,那么堆最终将增长 32M -> 64M -> 128M。

您也可以尝试这样做以使 VM 在增长堆方面不那么激进:

-XX:MinHeapFreeRatio=40 GC 后堆空闲的最小百分比以避免扩展。

此外,根据我几年前的试验记忆,加载的本机库的数量对最小占用空间有巨大的影响。如果我没记错的话,加载 java.net.Socket 增加了超过 15M(我可能没有)。


T
Thorbjørn Ravn Andersen

Sun JVM 需要大量内存用于 HotSpot,它映射到共享内存中的运行时库。

如果内存是一个问题,请考虑使用另一个适合嵌入的 JVM。 IBM 有 j9,还有使用 GNU 类路径库的开源“jamvm”。 Sun 还在 SunSPOTS 上运行 Squeak JVM,因此还有其他选择。


可以选择禁用热点吗?
也许。检查您使用的 JVM 的命令行选项。
r
runholen

减少资源有限的系统的堆空间的一种方法可能是使用 -XX:MaxHeapFreeRatio 变量。这通常设置为 70,并且是在 GC 收缩之前可用的堆的最大百分比。将其设置为较低的值,您将在例如 jvisualvm 分析器中看到较小的堆 sice 通常用于您的程序。

编辑:要为 -XX:MaxHeapFreeRatio 设置较小的值,您还必须设置 -XX:MinHeapFreeRatio 例如

java -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=25 HelloWorld

EDIT2:为启动并执行相同任务的真实应用程序添加了一个示例,一个使用默认参数,一个使用 10 和 25 作为参数。我没有注意到任何真正的速度差异,尽管理论上 java 在后一个示例中应该使用更多时间来增加堆。

https://i.stack.imgur.com/VyPgR.png

最后,最大堆为 905,已用堆为 378

https://i.stack.imgur.com/CGaWo.png

最后,最大堆为 722,已用堆为 378

这实际上有一些影响,因为我们的应用程序运行在远程桌面服务器上,许多用户可能会同时运行它。


V
VonC

只是一个想法,但您可以检查 a ulimit -v option 的影响。

这不是一个实际的解决方案,因为它会限制所有进程可用的地址空间,但这将允许您使用有限的虚拟内存检查应用程序的行为。


这正是我的问题所在。我的堆设置为 64M,但 linux 保留 204MB。如果我将 ulimit 设置为低于 204,则应用程序根本不会运行。
有趣:设置 ulimit 可能会对其他进程产生意想不到的副作用,解释为什么应用程序无法运行。
问题似乎是 Java 需要保留更多的虚拟内存,即使它不会使用它。在 Windows 中,使用的虚拟内存和 Xmx 设置相当接近。
你用 JRockit JVM 试过了吗?
由于 JVM 的内存分配是 Heap Allocation 和 Perm Size 的总和(第一个可以使用 -Xms 和 -Xmx 选项修复),你有没有尝试使用 -XX:PermSize 和 -XX:MaxPermSize 进行一些设置(默认从 32MB 到 64MB,取决于 JVM 版本) ?
P
Paul Tomblin

Sun 的 java 1.4 有以下参数来控制内存大小:

-Xmsn 指定内存分配池的初始大小,以字节为单位。此值必须是大于 1MB 的 1024 的倍数。附加字母 k 或 K 表示千字节,或附加 m 或 M 表示兆字节。默认值为 2MB。示例: -Xms6291456 -Xms6144k -Xms6m -Xmxn 指定内存分配池的最大大小(以字节为单位)。此值必须是大于 2MB 的 1024 的倍数。附加字母 k 或 K 表示千字节,或附加 m 或 M 表示兆字节。默认值为 64MB。示例:-Xmx83886080 -Xmx81920k -Xmx80m

http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/java.html

Java 5 和 6 还有更多。请参阅http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp


我遇到的问题不在于堆大小,而在于 Linux 分配的虚拟内存量
阅读 kdgregory 的解释。减少堆大小、“新大小”和其他可配置参数将减少 jvm 占用的 REAL 内存量。
他可能有一个合法的问题。一些应用程序(比如我写的一个)映射一个 1 GB 的文件,而一些系统只有 2 GB 的虚拟内存,其中一些被共享库填充。如果这是问题,他绝对应该禁用 DSO 随机化。 /proc 中有一个选项。
M
Marko

不,您无法配置 VM 所需的内存量。但是,请注意,这是虚拟内存,而不是常驻内存,因此如果不实际使用,它只会留在那里而不会造成伤害。

或者,您可以尝试其他一些 JVM,然后是 Sun 的,内存占用更小,但我在这里不建议。