首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

理解 JVM 如何使用 Windows 和 Linux 上的本机内存(4)调试方法和技术更新

理解 JVM 如何使用 Windows 和 Linux 上的本机内存(4)调试方法和技术更新

是什么在使用本机内存?一旦确定本机内存被耗尽,下一个逻辑问题是:是什么在使用这些内存?这个问题很难回答,因为在默认情况下,Windows 和 Linux 不会存储关于分配给特定内存块的代码路径的信息。
当尝试理解本机内存都到哪里去了时,您的第一步是粗略估算一下,根据您的 Java 设置,将会使用多少本机内存。如果没有对 JVM 工作机制的深入知识,很难得出精确的值,但您可以根据以下指南粗略估算一下:
  • Java 堆占用的内存至少为 -Xmx 值。
  • 每个 Java 线程需要堆栈空间。堆栈空间因实现不同而异,但是如果使用默认设置,每个线程至多会占用 756KB 本机内存。
  • 直接 ByteBuffer 至少会占用提供给 allocate() 例程的内存值。
如果总数比您的最大用户空间少得多,那么您很可能不安全。Java 运行时中的许多其他组件可能会分配大量内存,进而引起问题。但是,如果您的初步估算值与最大用户空间很接近,则可能存在本机内存问题。如果您怀疑存在本机内存泄漏,或者想要准确了解内存都到哪里去了,使用一些工具将有所帮助。
Microsoft 提供了 UMDH(用户模式转储堆)和 LeakDiag 工具来在 Windows 上调试本机内存增长(参见 )。这两个工具的机制相同:记录特定内存区域被分配给了哪个代码路径,并提供一种方式来定位所分配的内存不会在以后被释放的代码部分。我建议您查阅文章 “Umdhtools.exe:如何使用 Umdh.exe 发现 Windows 上的内存泄漏”,获取 UMDH 的使用说明(参见 )。在本文中,我将主要讨论 UMDH 在分析存在泄漏的 JNI 应用程序时的输出。
本文的  包含一个名为 LeakyJNIApp 的 Java 应用程序,它循环调用一个 JNI 方法来泄漏本机内存。UMDH 命令获取当前的本机堆的快照,以及分配每个内存区域的代码路径的本机堆栈轨迹快照。通过获取两个快照,并使用 UMDH 来分析差异,您会得到两个快照之间的堆增长报告。
对于 LeakyJNIApp,差异文件包含以下信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// _NT_SYMBOL_PATH set by default to C:\WINDOWS\symbols
//
// Each log entry has the following syntax:
//
// + BYTES_DELTA (NEW_BYTES - OLD_BYTES) NEW_COUNT allocs BackTrace TRACEID
// + COUNT_DELTA (NEW_COUNT - OLD_COUNT) BackTrace TRACEID allocations
//     ... stack trace ...
//
// where:
//
//     BYTES_DELTA - increase in bytes between before and after log
//     NEW_BYTES - bytes in after log
//     OLD_BYTES - bytes in before log
//     COUNT_DELTA - increase in allocations between before and after log
//     NEW_COUNT - number of allocations in after log
//     OLD_COUNT - number of allocations in before log
//     TRACEID - decimal index of the stack trace in the trace database
//         (can be used to search for allocation instances in the original
//         UMDH logs).
//

+  412192 ( 1031943 - 619751)    963 allocs     BackTrace00468

Total increase == 412192




重要的一行是 +  412192 ( 1031943 - 619751)    963 allocs     BackTrace00468。它显示一个 backtrace 进行了 963 次分配,而且分配的内存都没有释放 — 总共使用了 412192 字节内存。通过查看一个快照文件,您可以将 BackTrace00468 与有意义的代码路径关联起来。在第一个快照文件中搜索 BackTrace00468,可以找到如下信息:
1
2
3
4
5
000000AD bytes in 0x1 allocations (@ 0x00000031 + 0x0000001F) by: BackTrace00468
        ntdll!RtlpNtMakeTemporaryKey+000074D0
        ntdll!RtlInitializeSListHead+00010D08
        ntdll!wcsncat+00000224
        leakyjniapp!Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod+000000D6




这显示内存泄漏来自 Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 函数中的 leakyjniapp.dll 模块。
在编写本文时,Linux 没有类似于 UMDH 或 LeakDiag 的工具。但在 Linux 上仍然可以采用许多方式来调试本机内存泄漏。Linux 上提供的许多内存调试器可分为以下类别:
  • 预处理器级别。这些工具需要将一个头文件编译到被测试的源代码中。可以使用这些工具之一重新编译您自己的 JNI 库,以跟踪您代码中的本机内存泄漏。除非您拥有 Java 运行时本身的源代码,否则这种方法无法在 JVM 中发现内存泄漏(甚至很难在随后将这类工具编译到 JVM 等大型项目中,并且编译非常耗时)。Dmalloc 就是这类工具的一个例子(参见 )。
  • 链接程序级别。这些工具将被测试的二进制文件链接到一个调试库。再一次,尽管这对个别 JNI 库是可行的,但不推荐将其用于整个 Java 运行时,因为运行时供应商不太可能支持您运行修改的二进制文件。Ccmalloc 是这类工具的一个例子(参见 )。
  • 运行时链接程序级别。这些工具使用 LD_PRELOAD 环境变量预先加载一个库,这个库将标准内存例程替换为指定的版本。这些工具不需要重新编译或重新链接源代码,但其中许多工具与 Java 运行时不太兼容。Java 运行时是一个复杂的系统,可以以非常规的方式使用内存和线程,这通常会干扰或破坏这类工具。您可以试验一下,看看是否有一些工具适用于您的场景。NJAMD 是这类工具的一个例子(参见 )。
  • 基于模拟程序。Valgrind memcheck 工具是这类内存调试器的惟一例子(参见 )。它模拟底层处理器,与 Java 运行时模拟 JVM 的方式类似。可以在 Valgrind 下运行 Java 应用程序,但是会有严重的性能影响(速度会减慢 10 到 30 倍),这意味着难以通过这种方式运行大型、复杂的 Java 应用程序。Valgrind 目前可在 Linux x86、AMD64、PPC 32 和 PPC 64 上使用。如果您使用 Valgrind,请在使用它之前尝试使用最小的测试案例来将减轻性能问题(如果可能,最好移除整个 Java 运行时)。
对于能够容忍这种性能开销的简单场景,Valgrind memcheck 是最简单且用户友好的免费工具。它能够为泄漏内存的代码路径提供完整的堆栈轨迹,提供方式与 Windows 上的 UMDH 相同。
LeakyJNIApp 非常简单,能够在 Valgrind 下运行。当模拟的程序结束时,Valgrind memcheck 工具能够输出泄漏的内存的汇总信息。默认情况下,LeakyJNIApp 程序会一直运行,要使其在固定时期之后关闭,可以将运行时间(以秒为单位)作为惟一的命令行参数进行传递。
一些 Java 运行时以非常规的方式使用线程堆栈和处理器寄存器,这可能使一些调试工具产生混淆,这些工具要求本机程序遵从寄存器使用和堆栈结构的标准约定。当使用 Valgrind 调试存在内存泄漏的 JNI 应用程序时,您可以发现许多与内存使用相关的警告,并且一些线程堆栈看起来很奇怪,这是由 Java 运行时在内部构造其数据的方式所导致的,不用担心。
要使用 Valgrind memcheck 工具跟踪 LeakyJNIApp,(在一行上)使用以下命令:
1
2
valgrind --trace-children=yes --leak-check=full
java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIApp 10




--trace-children=yes 选项使 Valgrind 跟踪由 Java 启动器启动的任何进程。一些 Java 启动器版本会重新执行其本身(它们从头重新启动其本身,再次设置环境变量来改变行为)。如果您未指定 --trace-children,您将不能跟踪实际的 Java 运行时。
--leak-check=full 选项请求在代码运行结束时输出对泄漏的代码区域的完整堆栈轨迹,而不只是汇总内存的状态。
当该命令运行时,Valgrind 输出许多警告和错误(在此环境中,其中大部分都是无意义的),最后按泄漏的内存量升序输出存在泄漏的调用堆栈。在 Linux x86 上,针对 LeakyJNIApp 的 Valgrind 输出的汇总部分结尾如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
==20494== 8,192 bytes in 8 blocks are possibly lost in loss record 36 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494==
==20494==
==20494== 65,536 (63,488 direct, 2,048 indirect) bytes in 62 blocks are definitely
lost in loss record 42 of 45
==20494==    at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==    by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod
(in /home/andhall/LeakyJNIApp/libleakyjniapp.so)
==20494==    by 0x535CF56: ???
==20494==    by 0x46423CB: gpProtectedRunCallInMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x46441CF: signalProtectAndRunGlue
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x467E0D1: j9sig_protect
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9prt23.so)
==20494==    by 0x46425FD: gpProtectAndRun
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x4642A33: gpCheckCallin
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x464184C: callStaticVoidMethod
(in /usr/local/ibm-java2-i386-50/jre/bin/libj9vm23.so)
==20494==    by 0x80499D3: main
(in /usr/local/ibm-java2-i386-50/jre/bin/java)
==20494==
==20494== LEAK SUMMARY:
==20494==    definitely lost: 63,957 bytes in 69 blocks.
==20494==    indirectly lost: 2,168 bytes in 12 blocks.
==20494==      possibly lost: 8,600 bytes in 11 blocks.
==20494==    still reachable: 5,156,340 bytes in 980 blocks.
==20494==         suppressed: 0 bytes in 0 blocks.
==20494== Reachable blocks (those to which a pointer was found) are not shown.
==20494== To see them, rerun with: --leak-check=full --show-reachable=yes




堆栈的第二行显示内存是由 com.ibm.jtc.demos.LeakyJNIApp.nativeMethod() 方法泄漏的。
也可以使用一些专用调试应用程序来调试本机内存泄漏。随着时间的推移,会有更多工具(包括开源和专用的)被开发出来,这对于研究当前技术的发展现状很有帮助。
就目前而言,使用免费工具调试 Linux 上的本机内存泄漏比在 Windows 上完成相同的事情更具挑战性。UMDH 支持就地 调试 Windows 上本机内存泄漏,在 Linux 上,您可能需要进行一些传统的调试,而不是依赖工具来解决问题。
返回列表