尽管 Java™运行时能够解决大量的内存管理问题,但对程序的内存占用情况保持警惕仍然是优化机器性能、测定内存泄露的关键。Windows上有很多工具可以监控内存的使用。但每种工具各有长短,都有特定的倾向性,常常没有明确地定义自己测量的是什么。作者将澄清关于内存使用的一些常见误解,介绍很多有用的工具,同时还将提供何时以及如何使用它们的指南。
Java技术最知名的一个优点是:与其他语言如 C 程序员不同,Java 程序员不需要对令人畏惧的内存分配和释放负责。Java运行库可以为您管理这些任务。每个实例化的对象都自动在堆中分配内存,垃圾收集程序定期收回不再使用的对象所占据的内存。但是您还不能完全撒手不管。您仍然需要监控程序的内存使用情况,因为 Java 进程的内存不仅仅包括堆中分配的对象。它还包括程序的字节码(JVM在运行时解释执行的指令)、JIT 代码(已经为目标处理器编译过的代码)、任何本机代码和 JVM使用的一些元数据(异常表、行号表等等)。情况更为复杂的是,某些类型的内存(如本机库)可以在进程间共享,因此确定 Java应用程序的内存占用可能是一项非常艰巨的任务。
有大量在 Windows 监控内存使用的工具,但不幸的是没有一种能够提供您需要的所有信息。更糟的是,这些形形色色的工具甚至没有一个公共的词汇表。但本文会助您一臂之力,文中将介绍一些最有用的、可免费获得的工具,并提供了如何使用它们的技巧。
了解本文要讨论的工具之前,需要对 Windows 如何管理内存有基本的理解。Windows 使用一种 分页请求虚拟内存系统,现在我们就来分析一下这种系统。
虚拟内存的概念在上个世纪五十年代就提出了,当时是作为解决不能一次装入实际内存的程序这一复杂问题的方案提出的。在虚拟内存系统中,程序可以访问超出可用物理内存的更大的地址集合,专用内存管理程序将这些逻辑地址映射到实际地址,使用磁盘上的临时存储保存超出的部分。
Windows 所使用的现代虚拟内存实现中,虚拟存储被组织成大小相同的单位,称为 页。每个操作系统进程占用自己的 虚拟地址空间,即一组可以读写的虚拟内存页。每个页可以有三种状态:
- 自由:还没有进程使用这部分地址空间。如果企图访问这部分空间,无论读写都会造成某种运行时失效。该操作将导致弹出一个 Windows 对话框,提示出现了访问冲突。(Java 程序不会造成这种错误,只有用支持指针的语言编写的程序才可能造成这种问题。)
- 保留:这部分地址空间保留给进程,以供将来使用,但是在交付之前,不能访问该地址空间。很多 Java 堆在一开始处于保留状态。
- 提交:程序可以访问的内存,得到了完全 支持,就是说已经在分页文件中分配了页帧。提交的页只有在第一次被引用时才装入主存,因此成为 请求式分页。
图 1 说明了进程地址空间中的虚拟页如何映射到内存中的物理页帧。
图 1. 进程地址空间中的虚拟页到物理页帧的映射
![内存组织](https://i-blog.csdnimg.cn/blog_migrate/027900cdc290cd9ba8817abde9bc76e6.png)
如果运行的是 32 位机器(如一般的 Intel 处理器),那么进程的整个虚拟地址空间就是 4GB,因为这是用 32位所能寻址的最大地址空间。Windows 通常不会允许您访问地址空间中的所有这些内存,进程自己使用的只有不到一半,其他供 Windows使用。这 2 GB 的私有空间部分包含了 JVM 执行程序所需要的多数内存:Java 堆、JVM 本身的 C堆、用于程序线程的栈、保存字节码和即时编译方法的内存、本机方法所分配的内存等等。后面介绍地址空间映射时,我们将描述这些不同的部分。
希望分配了大量连续内存区域但这些内存不马上同时使用的程序常常结合使用保留内存和提交内存。JVM 以这种方式分配 Java 堆。参数 -mx
告诉 JVM 堆有多大,但 JVM 通常不在一开始就分配所有这些内存。它 保留 -mx
所规定的大小,标记能够提交的整个地址范围。然后它仅仅提交一部分内存,这也是内存管理程序需要在实际内存和分页文件中分配页来支持它们的那一部分。以后活动数据数量增加,堆需要扩展,JVM 可以再提交多一点内存,这些内存与当前提交的部分相邻。通过这种方式,JVM可以维护单一的、连续的堆空间,并根据需要增长(关于如何使用 JVM 堆参数请参阅 参考资料)。
物理存储页组织成大小相同的单位,通常称为 页帧。操作系统有一种数据结构称为 页表,将应用程序访问的虚拟页映射到主存中的实际页帧。没有装入的页保存在磁盘上的临时分页文件中。当应用程序要访问当前不在内存中的页时,就会出现 页面错误,导致内存管理程序从分页文件中检索该页并放到主存中,这个任务称为 分页。决定将哪些页交换出去的具体算法取决于所用的 Windows 版本,可能是最近最少访问算法的一种变体。同样要注意,Windows允许进程间共享页帧,比如 DLL 分配的页帧,常常被多个应用程序同时使用。Windows通过将来自不同地址空间的多个虚拟页映射到同一个物理地址来实现这种机制。
应用程序很高兴对所有这些活动一无所知。它只知道自己的虚拟地址空间。但是,如果当前在主存中的页面集(称为 驻留集)少于实际要使用的页面集(称为 工作集),应用程序的性能很快就会显著降低。(不幸的是,本文中您将看到,我们要讨论的工具常常交换使用这两个术语,尽管它们指的是完全不同的事物。)
![]() ![]() |
![]() |
我们首先考察两种最常见的工具:Task Manager 和 PerfMon。这两个工具都随 Windows 一起提供,因此由此起步比较容易。
Task Manager 是一种非常见的 Windows 进程监控程序。您可以通过熟悉的 Ctrl-Alt-Delete 组合键来启动它,或者右击任务栏。Processes 选项卡显示了最详细的信息,如图 2 所示。
图 2. Task Manager 进程选项卡
![TaskManager](https://i-blog.csdnimg.cn/blog_migrate/750e14ed191525718f72363835ddeae4.png)
图 2 中显示的列已经通过选择 View --> Select Columns 作了调整。有些列标题非常含糊,但可以在 Task Manager 帮助中找到各列的定义。和进程内存使用情况关系最密切的计数器包括:
- Mem Usage(内存使用):在线帮助将其称为进程的工作集(尽管很多人称之为驻留集)——当前在主存中的页面集。但是这个数值包含能够和其他进程共享的页面,因此要注意避免重复计算。比方说,如果要计算共享同一个 DLL 的两个进程的总内存占用情况,不能简单地把“内存使用”值相加。
- Peak Mem Usage(内存使用高峰值):进程启动以来 Mem Usage(内存使用)字段的最大值。
- Page Faults(页面错误):进程启动以来要访问的页面不在主存中的总次数。
- VM Size(虚拟内存大小):联机帮助将其称为“分配给进程私有虚拟内存总数。”更确切地说,这是进程所 提交的内存。如果进程保留内存而没有提交,那么该值就与总地址空间的大小有很大的差别。
虽然 Windows 文档将 Mem Usage(内存使用)称为工作集,但在该上下文中,它实际上指的是很多人所说的驻留集(resident set),明白这一点很重要。您可以在 Memory Management Reference 术语表(请参阅 参考资料)中找到这些术语的定义。 工作集 更通常的含义指的是一个逻辑概念,即在某一点上为了避免分页操作,进程需要驻留在内存中的那些页面。
随 Windows 一起提供的另一种 Microsoft 工具是 PerfMon,它监控各种各样的计数器,从打印队列到电话。PerfMon 通常在系统路径中,因此可以在命令行中输入 perfmon
来启动它。这个工具的优点是以图形化的方式显示计数器,很容易看到计数器随时间的变化情况。
请在 PerfMon 窗口上方的工具栏中单击 + 按钮,这样会打开一个对话框让您选择要监控的计数器,如图 3a 所示。计数器按照 性能对象分成不同的类别。与内存使用关系最密切的两个类是 Memory 和 Process。选中计数器然后单击 Explain 按钮,就可以看到计数器的定义。说明出现在主对话框下方弹出的单独的窗口中,如图 3b 所示。
图 3a. PerfMon 计数器窗口
![PerfMon 计数器窗口](https://i-blog.csdnimg.cn/blog_migrate/1e0d2b14c5fbe14118bfb8f2e0a9ce5f.png)
图 3b. 说明
![PerfMon 说明窗口](https://i-blog.csdnimg.cn/blog_migrate/e01528e0b8ef43e24c458cb58bc1634d.png)
选择感兴趣的计数器(使用 Ctrl 可以选中多行)和要监控的实例(所分析的应用程序的 Java 进程),然后单击 Add 按钮。工具立刻开始显示选择的所有计数器的值。您可以选择用报告、图表或者直方图来显示这些值。图 4 显示的是一个直方图。
图 4. PerfMon 直方图
![PerfMon 直方图](https://i-blog.csdnimg.cn/blog_migrate/0f3ab2692a5a2deb4ecb249e4d5eff8e.png)
如果图中什么也看不到,表明您可能需要改变比例,右击图形区域,选择 Properties 然后切换到 Graph 选项卡。也可以到计数器的 Data 选项卡改变某个计数器的比例。
要观察的计数器
不幸的是,PerfMon 使用了与 Task Manager 不同的术语。表 1 列出了最常用的计数器,如果有的话,还给出了相应的 Task Manager 功能:
表 1. 常用的 PerfMon 内存计数器
计数器名 | 类别 | 说明 | 等价的 Task Manager 功能 |
Working Set | Process | 驻留集,当前在实际内存中有多少页面 | Mem Usage |
Private Bytes | Process | 分配的私有虚拟内存总数,即提交的内存 | VM Size |
Virtual Bytes | Process | 虚拟地址空间的总体大小,包括共享页面。因为包含保留的内存,可能比前两个值大很多 | -- |
Page Faults / sec(每秒钟内的页面错误数) | Process(进程) | 每秒中出现的平均页面错误数 | 链接到 Page Faults(页面错误),显示页面错误总数 |
Committed Bytes(提交的字节数) | Memory(内存) | “提交”状态的虚 |