286

我对在 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:系统中运行着其他几个应用程序,这些应用程序是嵌入式的。而且系统确实有虚拟内存限制(来自评论,重要细节)。

4

8 回答 8

685

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

java -Xms1024m -Xmx4096m com.example.Hello

测量内存的不同方法

在 Linux 上,top命令为您提供了几个不同的内存数字。以下是关于 Hello World 示例的说明:

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

Windows 任务管理器的情况要复杂一些。在 Windows XP 下,有“Memory Usage”和“Virtual Memory Size”列,但官方文档没有说明它们的含义。Windows Vista 和 Windows 7 添加了更多列,并且它们实际上已记录在案。其中,“工作集”测量是最有用的;它大致对应于 Linux 上 RES 和 SHR 的总和。

了解虚拟内存映射

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

0000000040000000 36K rx--/usr/local/java/jdk-1.6-x64/bin/java
0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000 676K rwx-- [匿名]
00000006fae00000 21248K rwx-- [匿名]
00000006fc2c0000 62720K rwx-- [匿名]
0000000700000000 699072K rwx-- [匿名]
000000072aab0000 2097152K rwx-- [匿名]
00000007aaab0000 349504K rwx-- [匿名]
00000007c0000000 1048576K rwx-- [匿名]
...
00007fa1ed00d000 1652K r-xs-/usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000 1024K rwx-- [匿名]
00007fa1ed2d3000 4K ----- [匿名]
00007fa1ed2d4000 1024K rwx-- [匿名]
00007fa1ed3d4000 4K ----- [匿名]
...
00007fa1f20d3000 164K rx--/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 rx--/lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000 16K rx--/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 程序,关注堆中发生的事情要重要得多。占用的空间总量很重要,您可以采取一些步骤来减少它。更重要的是您在垃圾收集上花费的时间,以及堆的哪些部分被收集。

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

于 2009-02-18T15:17:07.777 回答
47

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
于 2015-03-09T04:17:31.977 回答
10

为 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(我可能没有)。

于 2009-02-18T16:21:52.190 回答
8

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

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

于 2009-03-03T17:14:16.813 回答
5

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

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

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

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

默认参数

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

最小堆 10,最大堆 25

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

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

于 2015-01-16T07:21:06.360 回答
4

只是一个想法,但您可以检查选项ulimit -v影响。

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

于 2009-02-18T14:51:39.430 回答
1

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

于 2009-02-18T14:35:14.820 回答
0

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

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

于 2009-02-18T14:28:52.307 回答