这一直是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程序,关注堆中发生的事情要重要得多。消耗的空间总量很重要,您可以采取一些步骤来减少这种情况。更重要的是您在垃圾回收中花费的时间,以及堆的哪些部分正在被收集。
访问磁盘(即数据库)成本高昂,内存成本低廉。如果您可以将一个换成另一个,请这样做。