这是Java长期以来的抱怨,但基本上没有意义,而且通常是基于查看错误的信息.通常的说法是"Hello World on Java需要10兆字节!为什么它需要这个?"好吧,下面是一种在64位JVM上创建Hello World的方法,声称它可以占用4GB以上的容量...至少通过一种形式的测量.
java -Xms1024m -Xmx4096m com.example.Hello
Different Ways to Measure Memory
在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进程,这通常仅限于共享库和内存映射文件.在这个例子中,我只运行了一个Java进程,所以我怀疑7k是操作系统使用库的结果.
- 默认情况下不会启用交换,此处也不会显示交换.它表示当前驻留在磁盘上的虚拟内存量,即whether or not it's actually in the swap space.操作系统非常擅长将活动页面保存在RAM中,交换的唯一解决方法是(1)购买更多内存,或(2)减少进程数量,因此最好忽略这个数字.
Windows任务管理器的情况要复杂一些.在Windows XP下,有"内存使用情况"和"虚拟内存大小"列,但official documentation没有说明它们的含义.Windows Vista和Windows 7添加了更多的列,它们实际上是documented.其中,"工作集"测量是最有用的;它大致相当于Linux上RES和SHR的总和.
Understanding the Virtual Memory Map
进程消耗的虚拟内存是进程内存映射中所有内容的总和.这包括数据(例如Java堆),但也包括程序使用的所有共享库和内存映射文件.在Linux上,您可以使用pmap命令查看映射到进程空间的所有内容(从这里开始,我只会提到Linux,因为我使用的是Linux;我相信Windows上也有类似的工具).以下是"Hello World"项目记忆 map 的节选;整个内存映射的长度超过pmap行,而且有pmap0行列表并不罕见.
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代码的共享库中.
- 一堆anon块保存Java堆和内部数据.这是一个Sun JVM,因此堆被分为多个代,每个代都是自己的内存块.请注意,JVM基于
-Xmx
值分配虚拟内存空间;这允许它有一个连续的堆.-Xms
值在内部用于表示程序启动时有多少堆处于"使用中",并在接近该限制时触发垃圾收集.
- 内存映射的JAR文件,在本例中是保存"JDK类"的文件在内存映射JAR时,可以非常高效地访问其中的文件(而不是每次从一开始就读取).Sun JVM将内存映射类路径上的所有JAR;如果应用程序代码需要访问JAR,还可以对其进行内存映射.
- 两个线程的每线程数据.
StackOverFlowError
万的挡路就是线程堆栈.我对4k的挡路没有很好的解释,但是@ericsoe认为它是一个"守卫挡路":它没有读/写权限,所以如果被访问就会导致段错误,jvm捕捉到这一点并将其翻译成StackOverFlowError
.对于一个真正的应用程序,您将在内存映射中看到数十个(如果不是数百个)这样的条目重复出现.
- 保存实际JVM代码的共享库之一.有好几种.
- C标准库的共享库.这只是JVM加载的严格意义上不属于Java的众多内容之一.
共享库特别有趣:每个共享库至少有两个段:一个只读段包含库代码,另一个读写段包含库的全局进程数据(我不知道没有权限的段是什么;我只在x64 Linux上见过).库的只读部分可以在使用库的所有进程之间共享;例如,libc
有150万个可共享的虚拟内存空间.
When is Virtual Memory Size Important?
虚拟内存映射包含很多东西.其中一些是只读的,一些是共享的,还有一些是分配的,但从未接触过(例如,本例中几乎所有4Gb的堆).但操作系统足够智能,只需加载所需的内容,因此虚拟内存大小在很大程度上无关紧要.
虚拟内存大小很重要的一点是,如果在32位操作系统上运行,则只能分配2Gb(或在某些情况下,3Gb)的进程地址空间.在这种情况下,您要处理的是一个稀缺的资源,可能需要做出权衡,例如减少堆大小,以便内存映射一个大文件或创建大量线程.
但是,考虑到64位机器无处不在,我认为虚拟内存大小很快就会成为一个完全不相关的统计数据.
When is Resident Set Size Important?
常驻集大小是虚拟内存空间中实际位于RAM中的部分.如果你的RSS在你的总物理内存中占据了相当大的一部分,那么也许是时候开始担心了.如果你的RSS增长到占用你所有的物理内存,你的系统开始交换,那么现在就不是开始担心的时候了.
但RSS也有误导性,尤其是在负载较轻的机器上.操作系统不会花费大量精力回收进程使用的页面.这样做没有什么好处,而且如果进程在将来触及页面,可能会出现代价高昂的页面错误.因此,RSS统计数据可能包含大量未被使用的页面.
Bottom Line
除非你是在交换,否则不要过分担心各种内存统计数据告诉你什么.警告:不断增长的RSS可能意味着某种内存泄漏.
对于Java程序,关注堆中发生的事情要重要得多.占用的总空间量很重要,您可以采取一些步骤来减少它.更重要的是您在垃圾收集上花费的时间,以及正在收集堆的哪些部分.
访问磁盘(即数据库)很昂贵,内存也很便宜.如果你可以用一个换另一个,那么就这样做.