这一直是对 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 程序,关注堆中发生的事情要重要得多。占用的空间总量很重要,您可以采取一些步骤来减少它。更重要的是您在垃圾收集上花费的时间,以及堆的哪些部分被收集。
访问磁盘(即数据库)很昂贵,而内存很便宜。如果您可以用一个换另一个,那么就这样做。