这是对 Java 的长期抱怨,但它基本上没有意义,并且通常基于查看错误的信息。通常的措辞类似于“Java 上的 Hello World 需要 10 兆字节!为什么需要这个?”好吧,这里有一种方法可以让 64 位 JVM 上的 Hello World 声称占用超过 4 GB 的空间……至少通过一种测量形式是这样。
java -Xms1024m -Xmx4096m com.example.Hello
测量内存的不同方法
在 Linux 上,top http://linux.die.net/man/1/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下,有“内存使用情况”和“虚拟内存大小”栏,但官方文档 http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/taskman_whats_there_w.mspx?mfr=true对他们的意思保持沉默。 Windows Vista 和 Windows 7 添加了更多列,它们实际上是有记录的 http://windows.microsoft.com/en-US/windows7/see-details-about-your-computers-performance-using-task-manager。其中,“工作集”测量是最有用的;它大致相当于 Linux 上 RES 和 SHR 的总和。
了解虚拟内存映射
进程消耗的虚拟内存是进程内存映射中所有内容的总和。这包括数据(例如,Java 堆),还包括程序使用的所有共享库和内存映射文件。在 Linux 上,您可以使用pmap http://linux.die.net/man/1/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 http://linux.die.net/man/2/mmap.
从顶部开始,我们有
- JVM 加载程序(即,当您键入
java
)。这是非常小的;它所做的只是加载到存储真实 JVM 代码的共享库中。
- 一堆保存 Java 堆和内部数据的匿名块。这是Sun JVM,因此堆被分成多代,每一代都是自己的内存块。请注意,JVM 根据以下方式分配虚拟内存空间:
-Xmx
价值;这允许它有一个连续的堆。这-Xms
value 在内部使用来表示程序启动时有多少堆“正在使用”,并在接近该限制时触发垃圾回收。
- 内存映射 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 程序,关注堆中发生的情况要重要得多。消耗的空间总量很重要,您可以采取一些步骤来减少空间消耗。更重要的是您在垃圾收集上花费的时间,以及堆的哪些部分被收集。
访问磁盘(即数据库)很昂贵,而内存很便宜。如果你可以用其中之一交换另一个,那就这样做吧。