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

273899 次浏览

不能配置虚拟机所需内存数量。但是,请注意,这是虚拟内存,而不是常驻内存,因此如果不实际使用,它只会保持在那里而不会受到损害。

提醒一下,您可以尝试其他JVM,而不是Sun JVM,内存占用更小,但我不能在这里建议。

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

< p > -Xmsn 指定内存分配池的初始大小,以字节为单位。 该值必须是1024的倍数 大于1MB。加上字母k 或K表示千字节,或m或m 表示兆字节。默认的 值为2MB。例子:< / p >
           -Xms6291456
-Xms6144k
-Xms6m
< p > -Xmxn 指定内存分配池的最大大小,以字节为单位。 该值必须是1024的倍数 大于2MB。加上字母k 或K表示千字节,或m或m 表示兆字节。默认的 值为64MB。例子:< / p >
           -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

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

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

这是Java长期以来的一个抱怨,但它在很大程度上是没有意义的,而且通常是基于查看错误的信息。通常的说法是“Java上的Hello World需要10兆字节!”它为什么需要这个?”好吧,这里有一种方法可以让Hello World在64位JVM上占用超过4g字节……至少从一种衡量方式来看是这样的。

java -Xms1024m -Xmx4096m com.example.Hello

测量内存的不同方法

在Linux上,命令为内存提供了几个不同的数字。下面是关于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行,而有一个1000行的列表是很正常的。

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堆和内部数据的anon块。这是一个Sun JVM,所以堆被分成多个代,每代都是自己的内存块。注意,JVM根据-Xmx值分配虚拟内存空间;这允许它拥有一个连续的堆。-Xms值在内部用于表示程序启动时有多少堆“正在使用”,并在接近该限制时触发垃圾收集。
  • 一个内存映射的JARfile,在本例中是保存“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程序,关注堆中发生的事情要重要得多。所消耗的空间总量很重要,您可以采取一些步骤来减少空间总量。更重要的是在垃圾收集上花费的时间,以及收集堆的哪些部分。

访问磁盘(即数据库)是昂贵的,而内存是便宜的。如果你可以用其中一个来交换另一个,那就这样做。

分配给Java进程的内存量与我所期望的相当。我在嵌入式/内存有限的系统上运行Java时也遇到过类似的问题。运行带有任意VM限制的任何应用程序或在没有足够交换量的系统上运行往往会崩溃。这似乎是许多现代应用程序的本质,它们的设计并不适合在资源有限的系统上使用。

您还可以尝试其他一些选项来限制JVM的内存占用。这可能会减少虚拟内存占用:

- xx:ReservedCodeCacheSize=32m保留代码缓存大小(字节)- maximum 代码缓存大小。(Solaris 64位, -server x86: 48m;在 1.5.0_06及之前版本,Solaris 64位和and64: 1024m.]

-XX:MaxPermSize=64m永久代的大小。[5.0及更新版本: 64位虚拟机扩展30%;1.4 amd64: 96;1.3.1 -client: 32m.]

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

你也可以尝试这样做,让VM不那么积极地增长堆:

-XX:MinHeapFreeRatio=40垃圾回收后的最小堆空闲百分比 避免扩张。< / p >

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

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

如果内存是个问题,可以考虑使用另一个适合嵌入的JVM。IBM有j9,还有使用GNU类路径库的开源“jamvm”。此外,Sun在sun黑子上运行了Squeak JVM,所以有替代方案。

使用-XX:MaxHeapFreeRatio变量是在资源有限的情况下减少系统堆的一种方法。这通常设置为70,是GC收缩堆之前的最大空闲百分比。将它设置为一个较低的值,您将在eg jvisualvm分析器中看到您的程序通常使用较小的堆块。

EDIT:为-XX:MaxHeapFreeRatio设置小值,您必须设置-XX:MinHeapFreeRatio 如< / p >

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

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

默认参数

最后,最大堆是905,使用堆是378

MinHeap 10, MaxHeap 25

最后,最大堆是722,使用堆是378

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

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
IBM有一篇关于设置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 < / p >

这篇博文说

常驻内存已知以类似于a的方式爬行

.内存泄漏或内存碎片

还有一个开放的JDK bug "glibc默认配置浪费内存"

在谷歌或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