内容


不要忘记内存

如何监控 Java 应用程序的 Windows 内存使用情况

Comments

Java 技术最知名的一个优点是:与其他语言如 C 程序员不同,Java 程序员不需要对令人畏惧的内存分配和释放负责。Java 运行库可以为您管理这些任务。每个实例化的对象都自动在堆中分配内存,垃圾收集程序定期收回不再使用的对象所占据的内存。但是您还不能完全撒手不管。您仍然需要监控程序的内存使用情况,因为 Java 进程的内存不仅仅包括堆中分配的对象。它还包括程序的字节码(JVM 在运行时解释执行的指令)、JIT 代码(已经为目标处理器编译过的代码)、任何本机代码和 JVM 使用的一些元数据(异常表、行号表等等)。情况更为复杂的是,某些类型的内存(如本机库)可以在进程间共享,因此确定 Java 应用程序的内存占用可能是一项非常艰巨的任务。

有大量在 Windows 监控内存使用的工具,但不幸的是没有一种能够提供您需要的所有信息。更糟的是,这些形形色色的工具甚至没有一个公共的词汇表。但本文会助您一臂之力,文中将介绍一些最有用的、可免费获得的工具,并提供了如何使用它们的技巧。

Windows 内存:一次旋风般的旅行

了解本文要讨论的工具之前,需要对 Windows 如何管理内存有基本的理解。Windows 使用一种 分页请求虚拟内存系统,现在我们就来分析一下这种系统。

虚拟地址空间

虚拟内存的概念在上个世纪五十年代就提出了,当时是作为解决不能一次装入实际内存的程序这一复杂问题的方案提出的。在虚拟内存系统中,程序可以访问超出可用物理内存的更大的地址集合,专用内存管理程序将这些逻辑地址映射到实际地址,使用磁盘上的临时存储保存超出的部分。

Windows 所使用的现代虚拟内存实现中,虚拟存储被组织成大小相同的单位,称为 。每个操作系统进程占用自己的 虚拟地址空间,即一组可以读写的虚拟内存页。每个页可以有三种状态:

  • 自由:还没有进程使用这部分地址空间。如果企图访问这部分空间,无论读写都会造成某种运行时失效。该操作将导致弹出一个 Windows 对话框,提示出现了访问冲突。(Java 程序不会造成这种错误,只有用支持指针的语言编写的程序才可能造成这种问题。)
  • 保留:这部分地址空间保留给进程,以供将来使用,但是在交付之前,不能访问该地址空间。很多 Java 堆在一开始处于保留状态。
  • 提交:程序可以访问的内存,得到了完全 支持,就是说已经在分页文件中分配了页帧。提交的页只有在第一次被引用时才装入主存,因此成为 请求式分页

图 1 说明了进程地址空间中的虚拟页如何映射到内存中的物理页帧。

图 1. 进程地址空间中的虚拟页到物理页帧的映射
内存组织
内存组织

如果运行的是 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

我们首先考察两种最常见的工具:Task Manager 和 PerfMon。这两个工具都随 Windows 一起提供,因此由此起步比较容易。

Task Manager

Task Manager 是一种非常见的 Windows 进程监控程序。您可以通过熟悉的 Ctrl-Alt-Delete 组合键来启动它,或者右击任务栏。Processes 选项卡显示了最详细的信息,如图 2 所示。

图 2. Task Manager 进程选项卡
TaskManager
TaskManager

图 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 术语表(请参阅 参考资料)中找到这些术语的定义。 工作集 更通常的含义指的是一个逻辑概念,即在某一点上为了避免分页操作,进程需要驻留在内存中的那些页面。

PerfMon

随 Windows 一起提供的另一种 Microsoft 工具是 PerfMon,它监控各种各样的计数器,从打印队列到电话。PerfMon 通常在系统路径中,因此可以在命令行中输入 perfmon 来启动它。这个工具的优点是以图形化的方式显示计数器,很容易看到计数器随时间的变化情况。

请在 PerfMon 窗口上方的工具栏中单击 + 按钮,这样会打开一个对话框让您选择要监控的计数器,如图 3a 所示。计数器按照 性能对象分成不同的类别。与内存使用关系最密切的两个类是 MemoryProcess。选中计数器然后单击 Explain 按钮,就可以看到计数器的定义。说明出现在主对话框下方弹出的单独的窗口中,如图 3b 所示。

图 3a. PerfMon 计数器窗口
PerfMon 计数器窗口
PerfMon 计数器窗口
图 3b. 说明
PerfMon 说明窗口
PerfMon 说明窗口

选择感兴趣的计数器(使用 Ctrl 可以选中多行)和要监控的实例(所分析的应用程序的 Java 进程),然后单击 Add 按钮。工具立刻开始显示选择的所有计数器的值。您可以选择用报告、图表或者直方图来显示这些值。图 4 显示的是一个直方图。

图 4. PerfMon 直方图
PerfMon 直方图
PerfMon 直方图

如果图中什么也看不到,表明您可能需要改变比例,右击图形区域,选择 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(内存)“提交”状态的虚拟内存总字节数

尝试一个例子

您可以下载并运行我们用 C 编写的一个小程序(请参阅 下载部分),来观察 Task Manager 和 PerfMon 中显示的这些数量。该程序首先调用 Windows VirtualAlloc 保留内存,然后再提交这些内存,最后使用其中一些内存,每 4,096 个字节写入一个值,从而将页面代入工作集。如果运行该例子,并使用 Task Manager 或 PerfMon 观察,就会发现这些值的变化情况。

网络上的有用工具

现在已经看到了应用程序使用多少内存,还需要深入分析内存的实际内容。这一节介绍一些更加复杂的工具,讨论什么时候适用输出结果,以及如何解释这些结果。

PrcView

PrcView 是我们要介绍的第一个可以观察进程地址空间内容的工具(请参阅 参考资料)。该工具不仅能用于观察内存占用,还可以设置优先级和杀死进程,还有一个很有用的命令行版本,用来列出机器上所有进程的属性。但我们要介绍的如何使用它观察内存占用情况。

启动 PrcView 会看到一个类 Task Manager 的视图,它显示了系统中的进程。如果滚动窗口并选中一个 Java 进程,屏幕就会如图 5 所示。

图 5. 启动后的 PrcView 窗口
PrcView 进程查看器
PrcView 进程查看器

右击该 Java 进程打开弹出菜单,或者从上方的菜单条中选择 Process,就可以看到该进程的一些情况,比如它拥有的线程、加载的 DLL,也可以杀死该进程或者设置其优先级。我们所关心的是考察其内存占用,打开如图 6 所示的窗口。

图 6. 观察进程的内存
PrcView 内存 1
PrcView 内存 1

现在我们分析一下 PrcView 显示的地址空间映射的前几行。第一行表明从地址 0 开始,有一块长度为 65,536 (64K) 的内存是自由空间。这些地址什么也没有分配,也不能用于寻址。第二行说明紧跟在后面,从地址 0x00010000 起,有一个长为 8,192 字节(两个 4K 页面)的提交内存,即可以寻址并且得到分页文件中的页帧支持的内存。然后是一段自由空间,另一段提交空间,如此等等。

碰巧的是,这些地址空间区域对您来说没有什么意义,因为它是供 Windows 使用的。描述 Windows 地址空间的 Microsoft 文档指出,这些不同的区域是为兼容 MS-DOS 保留的用户数据和代码所用的区域从 4MB 开始(请参阅 参考资料)。

向下滚动窗口,最终会看到某些您能够清楚识别的地址空间,如图 7 所示。

图 7. Java 堆
PrcView 内存 2
PrcView 内存 2

图 7 中高亮显示的行及其后的一行对应 Java 堆。我们给这里启动的 Java 进程 1000MB 大小的堆(使用 -mx1000m),对于该程序而言,这个堆太大了,但这样在 PrcView 映射中更加清楚。高亮显示的一行说明堆的提交部分只有 4MB,从地址 0x10180000 开始。紧随在后面的一行显示了一个很大的保留区域,这是堆中还没有提交的那一部分。在启动过程中,JVM 首先保留出完整的 1000MB 空间(从 0x10180000 到 0x4e980000 范围之内的地址都不能用),然后提交启动过程所需要的那一部分,该例中为 4MB。为了验证该值确实对应当前的堆大小,您可以用 -verbosegc JVM 选项调用该 Java 程序,可以打印出垃圾收集程序中的详细信息。从下面的 -verbosegc 输出中第二个 GC 的第二行可以看出,当前的堆大小大约是 4MB:

>java -mx1000m -verbosegc Hello
[ JVMST080: verbosegc is enabled ]
[ JVMST082: -verbose:gc output will be written to stderr ]
  <GC[0]: Expanded System Heap by 65536 bytes
  <GC(1): GC cycle started Wed Sep 29 16:55:44 2004
  <GC(1): freed 417928 bytes, 72% free (3057160/4192768), in 104 ms>
  <GC(1): mark: 2 ms, sweep: 0 ms, compact: 102 ms>
  <GC(1): refs: soft 0 (age >= 32), weak 0, final 2, phantom 0>
  <GC(1): moved 8205 objects, 642080 bytes, reason=4>

-verbosegc 的输出格式取决于所用的 JVM 实现,请参阅 参考资料中关于 IBM JVM 的相关文章,或者参考供应商的文档。

如果活动数据的数量增加,JVM 需要将堆的大小扩展到 4MB 之外,它就会提交稍微多一点的保留区域。就是说,新的区域可以从 0x10580000 开始,与已经提交的堆空间连接在一起。

图 7 所示的 PrcView 窗口中,最下面一行的三个总数给出了进程提交的总内存,这些数据是根据第七列 Type 计算得到的。三个总数为:

  • Private:分页文件支持的提交内存。
  • Mapped:直接映射到文件系统的提交内存。
  • Image:属于可执行代码的提交内存,包括启动的执行文件和 DLL。

到目前为止,我们只是在根据大小来了解堆在地址空间中的分配情况。为了更好的理解其他一些内存区域,如果能够观察内存的内部情形,会对您的了解很有帮助。这就要用到下面将讨论的工具 TopToBottom。

TopToBottom

TopToBottom 可以从 smidgeonsoft.com 免费获得(请参阅 参考资料)。该工具没有任何文档,但是为当前执行进程提供了一组完备的视图。您不仅能够按名称进程 ID 排序,还能够按起动时间排序,如果需要了解计算机上程序的启动顺序这一点可能很有用。

图 8 所示的 TopToBottom 窗口中,进程是按创建时间排序的( View --> Sort --> Creation Time)。

图 8. TopToBottom,进程按创建时间排序
TopToBottom 启动窗口
TopToBottom 启动窗口

StartUp 选项卡显示了创建 Java 进程的进程、开始的时间和日期、所用的命令行以及可执行文件和当前目录的完整路径。也可以单击 Environment 选项卡显示启动时传递给该进程的所有环境变量的值。Modules 选项卡显示了 Java 进程所用的 DLL,如图 9 所示。

图 9. TopToBottom Modules 选项卡
TopToBottom 模块
TopToBottom 模块

同样可以按照不同的方式对列表进行排序。在图 9 中,它们是按照初始化顺序排列的。如果双击其中的一行,可以看到 DLL 的详细信息:其地址和大小、编写的日期和时间、所依赖的其他 DLL 列表以及加载该 DLL 的所有运行中的进程列表。如果研究这个列表,就会发现有的 DLL 是每个运行的进程都要用到的,比如 NTDLL.DLL;有的在所有 Java 进程间共享,比如 JVM.DLL;而另有一些可能只有一个进程使用。

通过累加各个 DLL 的大小就可以计算出进程所用 DLL 的总大小。但是得到的结果可能会造成误解,因为它并不意味着进程要消费所有这些内存占用。真正的大小取决于进程实际使用了 DLL 的哪些部分。这些部分将进入进程的工作集。虽然很明显,但还是要注意 DLL 是只读的和共享的。如果大量进程都使用一个给定的 DLL,同一时刻只有一组实际内存页保存 DLL 数据。这些实际的页面可以映射到不同的地址,进入使用它们的那些进程。Task Manager 之类的工具将工作集看作是共享和非共享页面的总和,因此很难确定使用 DLL 对内存占用的影响。模块信息是一种很有用的方式,提供了“最差情况下”由于 DLL 造成的内存占用,需要的话可以使用其他工具作更详尽地分析。

我们关心的是内存占用情况,请单击 Memory 选项卡,图 10 显示了 Java 程序所用内存的一小部分。

图 10. TopToBottom Memory 选项卡
TopToBottom 内存
TopToBottom 内存

显示的内容和 PrcView 类似,但是它仅仅显示了虚拟空间中的提交内存,而没有保留内存。但是它有两个优点。首先,它可以更详尽地描述页面。比如在图 10 中专门标记了 Thread 3760 栈区域,而不仅仅是一些读/写数据。它是别的其他数据区包括环境、进程参数、进程堆、线程栈和线程环境块(TEB)。其次,您可以直接在 TopToBottom 中浏览甚至搜索内存。您可以搜索文本字符串或者最多 16 字节的十六进制序列。可以将十六进制搜索限制在特定的序列中,在检索地址引用时这一点很方便。

TopToBottom 也有快照功能,可以把进程的所有信息转储到剪贴板中。

VADump

VADump 是一种方便的命令行工具,属于 Microsoft ® Platform SDK 包(请参阅 参考资料)的一部分。它的目的是转储特定进程的虚拟地址空间和驻留集。使用 VADump 最简单的方法就是在命令行中输入以下命令:

vadump  
        process_id

process_id 是要分析的进程号。如果不带参数,则可以显示 VADump 完整的用法说明。我们建议您将结果通过管道保存到文件中(如 vadump 1234 > output.txt ),因为 VADump 生成的信息非常多,一屏放不下。

输出中首先给出进程虚拟地址空间的索引:

>vadump -p 3904
Address: 00000000 Size: 00010000
    State Free
Address: 00010000 Size: 00002000
    State Committed
    Protect Read/Write
    Type Private
Address: 00012000 Size: 0000E000
    State Free
Address: 00020000 Size: 00001000
    State Committed
    Protect Read/Write
    Type Private
Address: 00021000 Size: 0000F000
    State Free
Address: 00030000 Size: 00010000
    State Committed
    Protect Read/Write
    Type Private
Address: 00040000 Size: 0003B000 RegionSize: 40000
    State Reserved
    Type Private
................................

(为便于阅读,省略了部分行。)

对于每个块,都可以看到下列信息:

  • Address:十六进制格式,相对于进程虚拟地址空间起始位置的偏移量。
  • Size:字节数,用十六进制表示。
  • State:自由、保留或提交。
  • Protection status:只读或/读写。
  • Type:私有(不能被其他进程访问)、映射(直接来自文件系统)或镜像(可执行代码)。

然后列出进程使用的所有 DLL 及其大小,后面是工作集和分页文件使用的统计信息。

目前为止,所提到的信息都可以从其他工具获得。但是通过 VADump 的 -o 选项还可以得到更有启发作用的输出结果。它可以生成当前工作集的快照(某一给定时刻实际存在于主存中的页面)。关于该选项文档没有提供多少信息,但是在确定驻留集中最重要的部分时,这是一个极其有用的工具,这样能够确定最可能的内存优化目标。通过定期记录内存快照,您还可以使用它确定是否存在内存泄漏。这种模式下,输出从虚拟地址空间中提交页面的详尽转储开始,无论这些页面是否还在主存中:

>vadump -o -p 3904
0x00010000 (0) PRIVATE Base 0x00010000
0x00011000 (0) PRIVATE Base 0x00010000
0x00020000 (0) PRIVATE Base 0x00020000
0x00030000 (0) PRIVATE Base 0x00030000
0x00031000 (0) Private Heap 2
0x00032000 (0) Private Heap 2
0x00033000 (0) Private Heap 2
0x00034000 (0) Private Heap 2
0x00035000 (0) Private Heap 2
0x00036000 (0) Private Heap 2
0x00037000 (0) Private Heap 2
0x00038000 (0) Private Heap 2
0x00039000 (0) Private Heap 2
0x0003A000 (0) Private Heap 2
0x0003B000 (0) Private Heap 2
0x0003C000 (0) Private Heap 2
0x0003D000 (0) Private Heap 2
0x0003E000 (0) Private Heap 2
0x0003F000 (0) Private Heap 2
0x0007C000 (0) Stack for ThreadID 00000F64
0x0007D000 (0) Stack for ThreadID 00000F64
0x0007E000 (0) Stack for ThreadID 00000F64
0x0007F000 (0) Stack for ThreadID 00000F64
0x00080000 (7) UNKNOWN_MAPPED Base 0x00080000
0x00090000 (0) PRIVATE Base 0x00090000
0x00091000 (0) Process Heap
0x00092000 (0) Process Heap
0x00093000 (0) Process Heap
...........................

滚动到这个长长的列表的最后,您会看到更有趣的信息:目前驻留主存的进程页面的页表映射清单:

0xC0000000 > (0x00000000 : 0x003FFFFF)   132 Resident Pages
             (0x00280000 : 0x00286000) > jsig.dll
             (0x00290000 : 0x00297000) > xhpi.dll
             (0x002A0000 : 0x002AF000) > hpi.dll
             (0x003C0000 : 0x003D8000) > java.dll
             (0x003E0000 : 0x003F7000) > core.dll
             (0x00090000 : 0x00190000) > Process Heap segment 0
             (0x00190000 : 0x001A0000) > Private Heap 0 segment 0
             (0x001A0000 : 0x001B0000) > UNKNOWN Heap 1 segment 0
             (0x00380000 : 0x00390000) > Process Heap segment 0
             (0x00030000 : 0x00040000) > Private Heap 2 segment 0
             (0x00390000 : 0x003A0000) > Private Heap 3 segment 0
             (0x00040000 : 0x00080000) > Stack for thread 0
0xC0001000 > (0x00400000 : 0x007FFFFF)   13 Resident Pages
             (0x00400000 : 0x00409000) > java.exe
.................................................................

每个映射都对应页表中的一项,组成了进程工作集另一个 4KB。但要从这些映射中发现应用程序的哪些部分使用了最多的内存仍然很困难,但幸运的是下一部分输出给出了有用的总结:

Category                   Total       Private  Shareable  Shared
                       Pages   KBytes   KBytes   KBytes    KBytes
  Page Table Pages        20       80       80       0       0
  Other System            10       40       40       0       0
  Code/StaticData       1539     6156     3988    1200     968
  Heap                   732     2928     2928       0       0
  Stack                    9       36       36       0       0
  Teb                      5       20       20       0       0
  Mapped Data             30      120        0       0     120
  Other Data            1314     5256     5252       4       0
  Total Modules         1539     6156     3988    1200     968
  Total Dynamic Data    2090     8360     8236       4     120
  Total System            30      120      120       0       0
Grand Total Working Set 3659    14636    12344    1204    1088

最有趣的两个值通常是 Heap (即 Windows 进程堆)和 Other Data。直接通过调用 Windows API 分配的内存组成了进程堆部分,Other Data 中包括 Java 堆。Grand Total Working Set 对应 Task Manager 的 Mem Usage 和 TEB 字段(进程的线程环境块所需要的内存,TEB 是一种 Windows 内部结构)。

最后,在 VADump -o 输出的最下端总结了 DLL、堆和线程栈对工作集的相对贡献:

Module Working Set Contributions in pages
    Total   Private Shareable    Shared Module
        9         2         7         0 java.exe
       85         5         0        80 ntdll.dll
       43         2         0        41 kernel32.dll
       15         2         0        13 ADVAPI32.dll
       11         2         0         9 RPCRT4.dll
       53         6         0        47 MSVCRT.dll
      253        31       222         0 jvm.dll
        6         3         3         0 jsig.dll
        7         4         3         0 xhpi.dll
       15        12         3         0 hpi.dll
       12         2         0        10 WINMM.dll
       21         2         0        19 USER32.dll
       14         2         0        12 GDI32.dll
        6         2         0         4 LPK.DLL
       10         3         0         7 USP10.dll
       24        18         6         0 java.dll
       22        16         6         0 core.dll
       18        14         4         0 zip.dll
      915       869        46         0 jitc.dll
Heap Working Set Contributions
   6 pages from Process Heap (class 0x00000000)
	0x00090000 - 0x00190000 6 pages
   2 pages from Private Heap 0 (class 0x00001000)
	0x00190000 - 0x001A0000 2 pages
   0 pages from UNKNOWN Heap 1 (class 0x00008000)
	0x001A0000 - 0x001B0000 0 pages
   1 pages from Process Heap (class 0x00000000)
	0x00380000 - 0x00390000 1 pages
 715 pages from Private Heap 2 (class 0x00001000)
	0x00030000 - 0x00040000 15 pages
	0x008A0000 - 0x009A0000 241 pages
	0x04A60000 - 0x04C60000 450 pages
	0x054E0000 - 0x058E0000 9 pages
   1 pages from Private Heap 3 (class 0x00001000)
	0x00390000 - 0x003A0000 1 pages
   7 pages from Private Heap 4 (class 0x00001000)
	0x051A0000 - 0x051B0000 7 pages
Stack Working Set Contributions
   4 pages from stack for thread 00000F64
   1 pages from stack for thread 00000F68
   1 pages from stack for thread 00000F78
   1 pages from stack for thread 00000F7C
   2 pages from stack for thread 00000EB0

通过这种模式还可以用 VADump 获得两个或更多 Java 进程的总和内存占用情况(请参阅本文后面的 技巧和窍门)。

Sysinternals Process Explorer

更有用的内存分析工具来自 Sysinternals 公司(请参阅 参考资料)。其中一个工具是图形化的进程管理器,如图 11 所示,它可以作为 Task Manager 的高级代替品。

图 11. Process Explorer 进程树
Process Explorer
Process Explorer

Process Explorer 具有和 Task Manager 相同的功能。比方说,您可以得到整个系统性能的动态图形(通过 View --> System Information...),也可用类似的方式配置主进程视图中的列。在 Process --> Properties... 中,Process Explorer 提供了进程的更多信息,比如完整路径和命令行、线程、CPU 实用的动态图表和私有内存。它的用户界面非常好,如图 11 所示。它还可以观察 DLL 的信息和进程的句柄。您可以使用 Options --> Replace Task Manager 用 Process Explorer 代替默认的 Task Manager。

Sysinternals ListDLLs

还可以从 Sysinternals 下载两个命令行工具:ListDLLs 和 Handle。如果希望在脚本或者程序中集成某种形式的内存监控,这两个工具非常有用。

ListDLLs 用于观察 DLL,DLL 可能造成很多内存占用。使用之前请将其添加到路径中,并使用帮助选项获得用法说明。您可以用进程 ID 或进程名调用它。下面是我们的 Java 程序调用 DLL 的列表:

>listdlls -r 3904
ListDLLs V2.23 - DLL lister for Win9x/NT
Copyright (C) 1997-2000 Mark Russinovich
http://www.sysinternals.com
---------------------------------------------------------------------
java.exe pid: 3904
Command line: java -mx1000m -verbosegc Hello
Base        Size     Version	        Path
0x00400000  0x9000   141.2003.0005.0022  C:\WINDOWS\system32\java.exe
0x77f50000  0xa7000  5.01.2600.1217  C:\WINDOWS\System32\ntdll.dll
0x77e60000  0xe6000  5.01.2600.1106  C:\WINDOWS\system32\kernel32.dll
0x77dd0000  0x8d000  5.01.2600.1106  C:\WINDOWS\system32\ADVAPI32.dll
0x78000000  0x87000  5.01.2600.1361  C:\WINDOWS\system32\RPCRT4.dll
0x77c10000  0x53000  7.00.2600.1106  C:\WINDOWS\system32\MSVCRT.dll
0x10000000  0x178000 141.2004.0003.0001  C:\Java141\jre\bin\jvm.dll
### Relocated from base of 0x10000000:
0x00280000  0x6000   141.2004.0003.0001  C:\Java141\jre\bin\jsig.dll
### Relocated from base of 0x10000000:
0x00290000  0x7000   141.2004.0003.0001  C:\Java141\jre\bin\xhpi.dll
### Relocated from base of 0x10000000:
0x002a0000  0xf000   141.2004.0003.0001  C:\Java141\jre\bin\hpi.dll
0x76b40000  0x2c000  5.01.2600.1106  C:\WINDOWS\system32\WINMM.dll
0x77d40000  0x8c000  5.01.2600.1255  C:\WINDOWS\system32\USER32.dll
0x7e090000  0x41000  5.01.2600.1346  C:\WINDOWS\system32\GDI32.dll
0x629c0000  0x8000   5.01.2600.1126  C:\WINDOWS\system32\LPK.DLL
0x72fa0000  0x5a000  1.409.2600.1106  C:\WINDOWS\system32\USP10.dll
### Relocated from base of 0x10000000:
0x003c0000  0x18000  141.2004.0003.0001  C:\Java141\jre\bin\java.dll
### Relocated from base of 0x10000000:
0x003e0000  0x17000  141.2004.0003.0001  C:\Java141\jre\bin\core.dll
### Relocated from base of 0x10000000:
0x04a40000  0x12000  141.2004.0003.0001  C:\Java141\jre\bin\zip.dll
### Relocated from base of 0x10000000:
0x04df0000  0x3a1000 141.2004.0003.0001  C:\Java141\jre\bin\jitc.dll

也可以使用 listdlls -r java 命令,列出所有运行的 Java 进程及其使用的 DLL。

Sysinternals Handle

Handle 给出进程所用句柄(文件、套接字等)的列表。解压 Handle 下载文件,并将其添加到路径中,然后试着运行它。对于我们的 Java 程序,输出结果如下所示:

>handle -p 3904
Handle v2.2
Copyright (C) 1997-2004 Mark Russinovich
Sysinternals - www.sysinternals.com
------------------------------------------------------------------
java.exe pid: 3904 99VXW67\cem
    c: File          C:\wsappdev51\workspace\Scratch
   4c: File          C:\wsappdev51\workspace\Scratch\verbosegc.out
   50: File          C:\wsappdev51\workspace\Scratch\verbosegc.out
  728: File          C:\WebSphere MQ\Java\lib\com.ibm.mq.jar
  72c: File          C:\WebSphere MQ\Java\lib\fscontext.jar
  730: File          C:\WebSphere MQ\Java\lib\connector.jar
  734: File          C:\WebSphere MQ\Java\lib\jms.jar
  738: File          C:\WebSphere MQ\Java\lib\jndi.jar
  73c: File          C:\WebSphere MQ\Java\lib\jta.jar
  740: File          C:\WebSphere MQ\Java\lib\ldap.jar
  744: File          C:\WebSphere MQ\Java\lib\com.ibm.mqjms.jar
  748: File          C:\WebSphere MQ\Java\lib\providerutil.jar
  74c: File          C:\Java141\jre\lib\ext\oldcertpath.jar
  750: File          C:\Java141\jre\lib\ext\ldapsec.jar
  754: File          C:\Java141\jre\lib\ext\JawBridge.jar
  758: File          C:\Java141\jre\lib\ext\jaccess.jar
  75c: File          C:\Java141\jre\lib\ext\indicim.jar
  760: File          C:\Java141\jre\lib\ext\ibmjceprovider.jar
  764: File          C:\Java141\jre\lib\ext\ibmjcefips.jar
  768: File          C:\Java141\jre\lib\ext\gskikm.jar
  794: File          C:\Java141\jre\lib\charsets.jar
  798: File          C:\Java141\jre\lib\xml.jar
  79c: File          C:\Java141\jre\lib\server.jar
  7a0: File          C:\Java141\jre\lib\ibmjssefips.jar
  7a4: File          C:\Java141\jre\lib\security.jar
  7a8: File          C:\Java141\jre\lib\graphics.jar
  7ac: File          C:\Java141\jre\lib\core.jar

可以看出我们的进程中有一个句柄,该句柄指向类路径目录和几个 JAR 文件。事实上,该进程还有更多的句柄,但默认情况下该工具仅显示引用文件的句柄。使用 -a 参数就可以显示其他句柄:

>handle -a -p 3904
Handle v2.2
Copyright (C) 1997-2004 Mark Russinovich
Sysinternals - www.sysinternals.com
------------------------------------------------------------------
java.exe pid: 3904 99VXW67\cem
    c: File          C:\wsappdev51\workspace\Scratch
   4c: File          C:\wsappdev51\workspace\Scratch\verbosegc.out
   50: File          C:\wsappdev51\workspace\Scratch\verbosegc.out
  71c: Semaphore
  720: Thread        java.exe(3904): 3760
  724: Event
  728: File          C:\WebSphere MQ\Java\lib\com.ibm.mq.jar
  72c: File          C:\WebSphere MQ\Java\lib\fscontext.jar
  730: File          C:\WebSphere MQ\Java\lib\connector.jar
  734: File          C:\WebSphere MQ\Java\lib\jms.jar
  738: File          C:\WebSphere MQ\Java\lib\jndi.jar
  73c: File          C:\WebSphere MQ\Java\lib\jta.jar
  740: File          C:\WebSphere MQ\Java\lib\ldap.jar
  744: File          C:\WebSphere MQ\Java\lib\com.ibm.mqjms.jar
  748: File          C:\WebSphere MQ\Java\lib\providerutil.jar
  74c: File          C:\Java141\jre\lib\ext\oldcertpath.jar
  750: File          C:\Java141\jre\lib\ext\ldapsec.jar
  754: File          C:\Java141\jre\lib\ext\JawBridge.jar
  758: File          C:\Java141\jre\lib\ext\jaccess.jar
  75c: File          C:\Java141\jre\lib\ext\indicim.jar
  760: File          C:\Java141\jre\lib\ext\ibmjceprovider.jar
  764: File          C:\Java141\jre\lib\ext\ibmjcefips.jar
  768: File          C:\Java141\jre\lib\ext\gskikm.jar
  76c: Key           HKCU
  770: Semaphore
  774: Thread        java.exe(3904): 3964
  778: Event
  77c: Semaphore
  780: Semaphore
  784: Thread        java.exe(3904): 3960
  788: Event
  78c: Thread        java.exe(3904): 3944
  790: Event
  794: File          C:\Java141\jre\lib\charsets.jar
  798: File          C:\Java141\jre\lib\xml.jar
  79c: File          C:\Java141\jre\lib\server.jar
  7a0: File          C:\Java141\jre\lib\ibmjssefips.jar
  7a4: File          C:\Java141\jre\lib\security.jar
  7a8: File          C:\Java141\jre\lib\graphics.jar
  7ac: File          C:\Java141\jre\lib\core.jar
  7b0: Event
  7b4: Thread        java.exe(3904): 3940
  7b8: Event
  7bc: Semaphore
  7c0: Directory     \BaseNamedObjects
  7c4: Key           HKLM\SOFTWARE\Windows NT\Drivers32
  7c8: Semaphore
  7cc: Semaphore
  7d0: Event
  7d4: Desktop       \Default
  7d8: WindowStation \Windows\WindowStations\WinSta0
  7dc: Event
  7e0: WindowStation \Windows\WindowStations\WinSta0
  7e4: Event
  7e8: Section
  7ec: Port
  7f0: Directory     \Windows
  7f4: Key           HKLM
  7f8: Directory     \KnownDlls  
  7fc: KeyedEvent    \KernelObjects\CritSecOutOfMemoryEvent

如果关心内存的使用,句柄是一个重要因素,因为每个句柄都要消耗一些空间。具体的数量取决于操作系统版本和句柄的类型。一般而言,句柄不应该对内存占用产生很大影响。只要数一数该工具输出的行数,就可以判定句柄是不是太多,或者是否还在增长。无论出现哪种情况,都值得注意,建议进行更细致的分析。

技巧和窍门

现在您已经操作(不是双关语,handle 还有一个含义是句柄)了我们要介绍的所有工具,下面是您单独或一起使用这些工具,改进内存监控的一些方法。

寻找进程 ID

为了找到应用程序的进程 ID,以便在 VADump 这样的命令行工具中使用,请在 Task Manager 中打开 Applications 选项卡右击所关心的进程。选择 Go To Process,这样就会在 Processes 选项卡中看到对应的 ID。

确定一个 Java 进程

是否对那些都命名为 Java 或 javaw 的进程感到困惑,希望找出您要分析的那个进程?如果从 IDE 或脚本中启动 Java 进程,要确定使用了哪一个 JVM 和发送给 Java 进程的命令行参数可能很困难。这些信息可以在 TopToBottom Startup 选项卡中找到。您可以看到调用 JVM 使用的完整命令行和进程启动的时间。

确定大量占用句柄的进程

是否遇到过保存文件却得到提示说文件正被另一个进程使用的情况?或者尝试关闭您认为可靠的程序而得到错误消息的情况?您可以使用 SysInternals Process Explorer 工具的 Handle Search 功能发现谁在捣乱。只要打开 Search 对话框并输入文件名即可。ProcExp 将遍历所有打开的句柄,并确定相应的进程。最终常常会发现,关闭用户界面后,编辑器或者 Web 浏览器还留下一个小的存根进程在运行。

调查有多少内存被共享

您可以使用 VADump 的 -o 选项获得进程当前工作集的详细视图,以及有多少是共享的。获得一个 Java 程序在系统上运行的内存转储,然后再启动另一个并转储。只要比较每个结果的 Code/StaticData 部分,就会发现“Shareable”字节变成了“Shared”,从而稍微降低了内存占用的增加。

清理驻留集

Windows 实现了一种“清除”进程驻留集的策略,在其看起来不再有用的时候予以清除。为了说明这一点,打开 Task Manager 的 Processes 选项框,便可以看到要监控的应用程序进程,然后最小化应用程序窗口,看看 Mem Usage 字段发生了什么变化!

确定应用程序需要的最少内存

对于 Windows Server 2003 和 Windows NT,Microsoft 提供了一个有趣的称为 ClearMem 的工具,如果希望进一步研究 Windows 下应用程序使用内存的情况,它可能非常有用(请参阅 参考资料)。该工具确定了实际内存的大小,分配足够的内存,很快地占用分配的内存然后将其释放。这样就增加了其他应用程序的内存占用压力,反复运行 ClearMem 的结果是迫使应用程序占用的内存数量减少到最小。

结束语

本文简要介绍了 Windows 如何管理内存,考察了一些最有用的免费工具,您可以用这些工具监控 Java 应用程序的内存使用。无疑您还会发现和使用其他的工具,无论从 Web 上免费下载产品还是购买商业产品,我们都希望澄清相互矛盾的术语会对您有所帮助。通常要确定您测量的目标的惟一方法就是做试验,比如我们用于示范 Task Manager 的 VM Size(虚拟内存大小)和 Mem Usage(内存使用)含义的 C 程序。

当然这些工具只能帮助确定问题的所在,如何解决还要靠您自己。多数时候您会发现 Java 堆获取了内存的一大部分,您需要深入分析代码,确定对象引用是否超出了必要的时间。这方面有更多的工具和文章可以提供帮助, 参考资料部分给出了一些有用的链接,可以为您指出正确的方向。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=49160
ArticleTitle=不要忘记内存
publish-date=11162004