内存详解


内存详解

理解 JVM 如何使用 AIX 上的本机内存

Java™堆耗尽并不是造成 java.lang.OutOfMemoryError的惟一原因。如果 本机内存耗尽,则会发生普通调试技巧无法解决的 OutOfMemoryError。本文将讨论本机内存的概念,Java 运行时如何使用它,它被耗尽时会出现什么情况,以及如何在 AIX®上调试本机OutOfMemoryError 。针对 Linux®和 Windows®系统的相同主题将在 另一篇同类文章中介绍。

Andrew Hall, 软件工程师, IBM

2009 年 5 月 11 日

  • +内容

Java 堆(每个 Java 对象在其中分配)是您在编写 Java 应用程序时使用最频繁的内存区域。JVM 设计用于将我们与主机的特性隔离,所以将内存当作堆来考虑再正常不过了。您一定遇到过 Java 堆 OutOfMemoryError,它可能是由于对象泄漏造成的,也可能是因为堆的大小不足以存储所有数据,您也可能了解这些场景的一些调试技巧。但是随着您的 Java 应用程序处理越来越多的数据和越来越多的并发负载,您可能就会遇到无法使用常规技巧进行修复的 OutOfMemoryError。在一些场景中,即使 java 堆未满,也会抛出错误。当这类场景发生时,您需要理解 Java 运行时环境(Java Runtime Environment,JRE)内部到底发生了什么。

Java 应用程序在 Java 运行时的虚拟化环境中运行,但是运行时本身是使用 C 之类的语言编写的本机程序,它也会耗用本机资源,包括 本机内存。本机内存是可用于运行时进程的内存,它与 Java 应用程序使用的 java 堆内存不同。每种虚拟化资源(包括 Java 堆和 Java 线程)都必须存储在本机内存中,虚拟机在运行时使用的数据也是如此。这意味着主机的硬件和操作系统施加在本机内存上的限制会影响到 Java 应用程序的性能。

本系列文章共分两篇,讨论不同平台上的相应话题。本文是其中一篇。在这两篇文章中,您将了解什么是本机内存,Java 运行时如何使用它,本机内存耗尽之后会发生什么情况,以及如何调试本机 OutOfMemoryError。本文将讨论 AIX 并专注于 IBM®Developer Kit for Java。另一篇 类似的文章讨论 Windows 和 Linux 上的这一主题,并且不会介绍任何特定的 Java 运行时。

本机内存简介

我将首先解释一下操作系统和底层硬件给本机内存带来的限制。如果您熟悉使用 C 等语言管理动态内存,那么您可以直接跳到 下一节

硬件限制

本机进程遇到的许多限制都是由硬件造成的,而与操作系统没有关系。每台计算机都有一个处理器和一些随机存取存储器(RAM),后者也称为物理内存。处理器将数据流解释为要执行的指令,它拥有一个或多个处理单元,用于执行整数和浮点运算以及更高级的计算。处理器具有许多 寄存器—— 常快速的内存元素,用作被执行的计算的工作存储,寄存器大小决定了一次计算可使用的最大数值。

处理器通过内存总线连接到物理内存。物理地址(处理器用于索引物理 RAM 的地址)的大小限制了可以寻址的内存。例如,一个 16 位物理地址可以寻址 0x0000 到 0xFFFF 的内存地址,这个地址范围包括 2^16 = 65536 个惟一的内存位置。如果每个地址引用一个存储字节,那么一个 16 位物理地址将允许处理器寻址 64KB 内存。

处理器被描述为特定数量的数据位。这通常指的是寄存器大小,但是也存在例外,比如 32 位 390 指的是物理地址大小。对于桌面和服务器平台,这个数字为 31、32 或 64;对于嵌入式设备和微处理器,这个数字可能小至 4。物理地址大小可以与寄存器带宽一样大,也可以比它大或小。如果在适当的操作系统上运行,大部分 64 位处理器可以运行 32 位程序。

表 1 列出了一些流行的架构以及它们的寄存器和物理地址大小:

表 1. 一些流行处理器架构的寄存器和物理地址大小
架构 寄存器带宽(位) 物理地址大小(位)
(现代)Intel®x86 32 32
36,具有物理地址扩展(Pentium Pro 和更高型号)
x86 64 64 目前为 48 位(以后将会增大)
PPC64 64 在 POWER 5 上为 50 位
390 31 位 32 31
390 64 位 64 64

操作系统和虚拟内存

如果您编写无需操作系统,直接在处理器上运行的应用程序,您可以使用处理器可以寻址的所有内存(假设连接到了足够的物理 RAM)。但是要使用多任务和硬件抽象等特性,几乎所有人都会使用某种类型的操作系统来运行他们的程序。

在 Aix 等多任务操作系统中,有多个程序在使用系统资源。需要为每个程序分配物理内存区域来在其中运行。可以设计这样一个操作系统:每个程序直接使用物理内存,并且可以可靠地仅使用分配给它的内存。一些嵌入式操作系统以这种方式工作,但是这在包含多个未经过集中测试的应用程序的环境中是不切实际的,因为任何程序都可能破坏其他程序或者操作系统本身的内存。

虚拟内存允许多个进程共享物理内存,而且不会破坏彼此的数据。在具有虚拟内存的操作系统(比如 Windows、Linux 和许多其他操作系统)中,每个程序都拥有自己的虚拟地址空间 —— 一个逻辑地址区域,其大小由该系统上的地址大小规定(所以,桌面和服务器平台的虚拟地址空间为 31、32 或 64 位)。进程的虚拟地址空间中的区域可被映射到物理内存、文件或任何其他可寻址存储。操作系统可以将物理内存中的数据移动到未使用的交换区,以便于最充分地利用物理内存。当程序尝试使用虚拟地址访问内存时,操作系统结合片上硬件将该虚拟地址映射到物理位置。该位置可以是物理 RAM、文件或交换区。如果一个内存区域被移动到交换空间,那么它将在被使用之前加载回物理内存中。图 1 展示了虚拟内存如何将进程地址空间区域映射到共享资源:

图 1. 虚拟内存将进程地址空间映射到物理资源
虚拟内存映射

本机程序的每个实例都作为进程运行。在 AIX 上,进程是关于 OS 控制资源(比如文件和套接字信息)、虚拟地址空间以及至少一个执行线程的一系列信息。

虽然 32 位地址可以引用 4GB 数据,但程序不能独自使用整个 4GB 地址空间。与其他操作系统一样(比如 Windows 和 Linux),地址空间分为多个部分,程序只能使用其中的一些部分;其余部分供操作系统使用。与 Windows 和 Linux 相比,AIX 内存模型更加复杂并且可以更加精确地进行优化。

AIX 32 位内存模型被分成 16 个 256MB 分段进行管理。图 2 显示了默认 32 位 AIX 内存模型的布局:

图 2. 默认 AIX 内存模型
默认 AIX 内存模型

不同分段的作用如下:

  • 分段 0:AIX 内核数据(用户程序不能直接访问)
  • 分段 1:应用程序文本(可执行代码)
  • 分段 2:线程栈和本机堆(通过 malloc/free 控制的区域)
  • 分段 3-C 和 E:内存映射区域(包括文件)和共享内存
  • 分段 D:共享库文本(可执行代码)
  • 分段 F:共享库数据

用户程序只能直接控制 16 个分段中的 12 个 —即 4GB 中的 3GB。最大的限制是,本机堆和所有线程栈都保存在分段 2 中。为了适应对数据需求较高的程序,AIX 提供了一个 大内存模型

大内存模型允许程序员或用户附加一些共享 / 映射分段作为本机堆使用,通过在构建可执行程序时提供一个链接器选项或者在程序启动之前设置 LDR_CNTRL环境变量。要在运行时支持大内存模型,需要设置 LDR_CNTRL=MAXDATA=0xN0000000。其中,N位于 1和 8之间。超过此范围的任何值都会造成操作系统使用默认内存模型。在大内存模型中,本机堆从分段 3 开始;分段 2 仅用于原始(初始)线程栈。

当您使用大内存模型时,分段分配是静态的;也就是说,如果你请求 4 个数据分段(1GB 本机堆),但是仅分配 1 个本机堆分段(256MB),则其他 3 个数据分段将不能用于内存映射。

如果您希望本机堆大于 2GB,并且运行的是 AIX 5.1 或更高版本,那么您可以使用 AIX 超大内存模型。与大内存模型类似,可以通过一个链接器选项或在运行时使用 LDR_CNTRL环境变量来为编译时的可执行程序启用超大内存模型。要在运行时启用超大内存模型,需要设置LDR_CNTRL=MAXDATA=0xN0000000@DSA。其中,N位于 0和 D之间(如果您使用 AIX 5.2 或更高版本),或位于 1和 A之间(如果您使用 AIX 5.1)。N值指定可用于本机堆的分段数量,但与大内存模型不同,这些分段可以在必要时用于映射。

通常,IBM Java 运行时使用超大内存模型,除非它被 LDR_CNTRL环境变量覆盖。

将 N设置为 1和 A之间,这会使用 3 和 C 之间的分段作为本机存储。在 AIX 5.2 中,将 N设置为 B或更多会更改内存布局 —它不再使用 D 和 F 作为共享库,并且允许它们用于本机存储或映射。将 N设置为 D可分配最多 13 个分段(3.25GB)的堆。将 N设置为 0可允许分段 3 到 F 用于映射 —本机堆保存在分段 2 中。图 3 展示了不同 AIX 内存模型所使用的不同地址空间布局:

图 3. 各种 MAXDATA值的 AIX 内在模型
默认 AIX 内存模型

本机内存泄漏或本机内存过度使用会造成各种问题,这取决于您是耗尽了地址空间还是用完了物理内存。耗尽地址空间通常只发生在 32 位进程中 —因为可以轻松地分配最大 4GB 地址空间。64 位进程的用户空间可以达到上千 GB,并且难以用完。如果您确实耗尽了 Java 进程的地址空间,则 Java 运行时会开始出现一些奇怪的症状,本文将在稍后讨论这些情况。在进程地址空间大于物理内存的系统中,内存泄漏或本机内存过度使用会迫使操作系统提供一些虚拟地址空间。访问操作系统提供的内存地址要比读取(物理内存中的)常驻地址慢很多,因为必须硬盘驱动器加载它。

如果您同时尝试使用过多 RAM 虚拟内存,造成数据无法存储在物理内存中,则系统挂起(thrash)—也就是花费大多数时间在交换空间与内存之间来回复制数据。出现这种情况时,计算机和各应用程序的性能将变得很差,用户会立即觉察到出现了问题。当 JVM 的 Java 堆被换出时,垃圾收集器的性能将变得极差,甚至会造成应用程序挂起。如果多个 Java 运行时在一台机器上同时运行,则物理内存必须满足所有 Java 堆的需要。

Java 运行时如何使用本机内存

Java 运行时是一个 OS 进程,它受上一节所提到的硬件及操作系统限制。运行时环境提供由一些未知用户代码驱动的功能;这使得无法预测运行时环境在各种情况下需要哪些资源。Java 应用程序在托管 Java 环境中采取的每一个措施都有可能影响提供该环境的运行时的资源需求。本节讨论 Java 应用程序消耗本机内存的方式及原因。

Java 堆和垃圾收集

Java 堆是分配给对象的内存区。IBM Developer Kits for Java Standard Edition 拥有一个物理堆,但一些专门的 Java 运行时,比如 IBM WebSphere Real Time,则有多个堆。堆可以分为多个部分,例如 IBM gencon 策略的 nursery和 tenured区。大多数 Java 堆都是作为本机内存的相邻 slab 实现的。

控制堆大小的方法是在 Java 命令行中使用 -Xmx和 -Xms选项(mx是堆的最大大小,ms是初始大小)。虽然逻辑堆(活跃使用的内存区)将根据堆中对象的数量和垃圾收集(CG)所花费的时间增大或缩小,但所使用的本机内存量仍然保持不变,并且将由 -Xmx值(最大堆大小)决定。内存管理器依赖作为相邻内存 slab 的堆,因此当堆需要扩展时无法分配更多本机内存;所有堆内存必须预先保留。

保留本机内存与分配它不同。保留本机内存时,它不受物理内存或其他存储的支持。虽然保留地址空间块不会耗尽物理资源,但它确实能防止内存用于其他目的。保留从未使用的内存造成的泄漏与已分配内存的泄漏同样严重。

AIX 上的 IBM 垃圾收集器将最大限度减少物理内存的使用,当使用的堆区域减少时,它会释放堆的备份存储。

对于大多数 Java 应用程序,Java 堆是最大的进程地址空间使用者,因此 Java 启动程序使用 Java 堆大小来确定如何配置地址空间。表 2 列出了不同堆大小范围的默认内存模型配置。您可以覆盖内存模型,方法是在启动 Java 启动程序之前设置 LDR_CNTRL环境变量。如果您正嵌入 Java 运行时或编写自己的启动程序,则需要自己配置内存模型 —通过指定适当的链接器标记或在启动 Java 启动程序之前设置 LDR_CNTRL

表 2. 不同堆大小的默认 LDR_CNTRL设置
堆范围 LDR_CNTRL 设置 最大本机堆大小 最大映射空间(不占用本机堆)
-Xmx0M to -Xmx2304M MAXDATA=0xA0000000@DSA 2.5GB 512MB
-Xmx2304M to -Xmx3072M MAXDATA=0xB0000000@DSA 2.75GB 512MB
> -Xmx2304M MAXDATA=0x0@DSA 256MB 3.25GB

即时(Just-in-time,JIT)编译器

JIT 编译器在运行时将 Java 字节码编译为优化的二进制码。这将极大地改善 Java 运行时的速度,并允许 Java 应用程序的运行速度能与本机代码相提并论。

编译字节码将使用本机内存(就像静态编译器一样,比如 gcc,需要内存才能运行),但是 JIT 的输出(可执行代码)也可以存储在本机内存中。包含许多经过 JIT 编译的方法的 Java 应用程序比较小的应用程序使用更多本机内存。

类和类加载器

Java 应用程序由定义对象结构和方法逻辑的类组成。它们还使用 Java 运行时类库中的类(比如 java.lang.String),并且可以使用第三方库。这些类需要在它们的使用期间存储在内存中。

Java 5 之后的 IBM 实现为各类加载器分配本机内存 slab,用于存储类数据。Java 5 中的共享类技术将共享内存中的某个区域映射到存储只读(因此可以共享)类数据的地址空间。当多个 JVM 在同一台机器上运行时,这将减少存储类数据所需的物理内存量。共享类还可以改善 JVM 的启动时间。

共享类系统将固定大小的共享内存区域映射到地址空间。可以不完全占用共享类缓存,并且其中还可以包含当前未使用的类(由其他 JVM 载入),因此使用共享类将比未使用共享类占用更多地址空间(但物理内存较少)。需要重点注意,共享类不能防止类加载器取消加载 —但它会造成类数据的一个子集保留在类缓存中。参见 参考资料了解更多关于共享类的信息。

加载更多类需要使用更多本机内存。每个类加载器还有各自的本机内存开销 —因此让许多类加载分别加载一个类会比让一个类加载器许多类使用更多本机内存。记住,不仅您的应用程序类需要占用内存;框架、应用服务器、第三方库和 Java 运行时都包含根据需要加载且占用空间的类。

Java 运行时可以卸载类以回收空间,但仅限于一些严格的条件下。不能卸载单个类;而应卸载类加载器,其对象是加载的所有类。卸载类加载器的条件仅限于:

  • Java 堆未包含到表示该类加载器的 java.lang.ClassLoader对象的引用。
  • Java 堆未包含到表示该类加载器加载的类的任何 java.lang.Class对象的引用。
  • 该类加载器加载的任何类的对象在 Java 堆中都处于非活动状态(即未被引用)。

注意,Java 运行时为所有 Java 应用程序创建的 3 个类默认加载器 —bootstrapextension和 application—永远都无法满足这些标准;因此,通过应用程序类加载器加载的任何系统类(比如 java.lang.String)或任何应用程序类都不能被释放。

即使类加载器可用于收集,但运行时只将类加载器作为 GC 周期的一部分进行收集。IBM gencon GC 策略(通过 -Xgcpolicy:gencon命令行参数启用)仅卸载主要(tenured)收集上的类加载器。如果某个应用程序正在运行 gencon 策略并创建和释放许多类加载器,则您会发现大量本机内存在 tenured 收集期间由可收集的类加载器保存。参见 参考资料,了解关于不同 IBM GC 策略的更多信息。

还可以在运行时生成类,而不需要您释放它。许多 JEE 应用程序使用 JavaServer Pages (JSP) 技术生成 Web 页面。使用 JSP 为执行的各个 . jsp 页面生成类,该类的持续时间为加载它们的类加载器的生存期 —通常为 Web 应用程序的生存期。

生成类的另一个种常用方法是使用 Java 反射。使用 java.lang.reflectAPI 时,Java 运行时必须将反射对象的方法(比如java.lang.reflect.Field)连接到被反射的对象或类。这种 “访问方法” 可以使用 Java Native Interface (JNI),它需要的设置非常少但运行缓慢,或者它可以在运行时动态地为您希望反射的各对象类型构建一个类。后一种方法设置较慢,但运行更快,因此它对于经常反射特定类的应用程序非常理想。

在最初几次反射类时,Java 运行时使用 JNI 方法。但是在使用了几次之后,访问方法将扩展到字节访问方法中,该方法涉及构建一个类并通过一个新的类加载器来加载它。执行大量反射会造成创建许多访问程序类和类加载器。保留到反射对象的引用会造成这些类保持为活动状态并继续占用空间。由于创建字节码访问程序相当慢,因此 Java 运行时可以缓存这些访问程序供稍后使用。一些应用程序和框架还缓存反射对象,因此会增加它们的本机内存占用。

您可以使用系统属性控制反射访问程序行为。IBM Developer Kit for Java 5.0 的默认扩展阀值(JNI 存取器在扩展到字节码存取器中之前的使用次数)是 15。您可以通过设置 sun.reflect.inflationThreshold系统属性来修改该值。您可以在 Java 命令行中通过 -Dsun.reflect.inflationThreshold=N来设置它。如果您将 inflationThreshold设置为 0或更小的值,则存取器将永远不会扩展。如果您发现应用程序要创建许多 sun.reflect.DelegatingClassloader(用于加载字节码存取器的类加载器),则这种设置非常有用。

另一种(极易造成误解的)设置也会影响反射存取器。-Dsun.reflect.noInflation=true会完全禁用扩展,但它会造成字节码存取器滥用。使用 -Dsun.reflect.noInflation=true会增加反射类加载器占用的地址空间量,因为会创建更多的类加载器。

您可以通过 javacore 转储来测量类和 JIT 代码在 Java 5 及以上版本中使用了多少内存。javacore 是一个纯文本文件,它包含转储发生时 Java 运行时的内部状态的概述 —包括关于已分配本机内存分段的信息。较新版本的 IBM Developer Kit for Java 5 和 6 将内存使用情况归讷在 javacore 中,对于较老版本(Java 5 SR10 和 Java 6 SR3 之前),本文的示例代码包包括一个 Perl 脚本,可以用于分配和呈现数据(参见 下载)。如果要运行它,您需要 Perl 解释器,它可以用于 AIX 和其他平台。参见 参考资料了解详细信息。

Javacores 将在抛出 OutOfMemoryError时生成(地址空间耗尽时可能会出现这种情况)。您还可以通过向 Java 进程发送一个 SIGQUIT来触发此事件(kill -3 <pid>)。要获取内存分段使用的概要信息,运行:

 perl get_memory_use.pl javacore.<date>.<time>.<pid>.txt

脚本的输出如下所示:

 perl get_memory_use.pl javacore.20080111.081905.1311.txt 
 Segment Usage           Reserved Bytes 
 Class Memory            281767824 
 Internal Memory         25763872 
 JIT Code Cache          67108864 
 JIT Data Cache          33554432 
 Object Memory           536870912

JNI

JNI 允许本机代码(使用 C 和 C++ 等本机语言编写的应用程序)调用 Java 方法,反之亦然。Java 运行时本身在很大程度上依赖 JNI 代码实现类库功能,例如文件和网络 I/O。JNI 应用程序可以通过三种方式增加 Java 运行时的本机内存占用:

  • JNI 应用程序的本机代码编译成加载到进程地址空间中的共享库或可执行程序。较大的本机应用程序在加载时就会占用大量的进程地址空间。
  • 本机代码必须与 Java 运行时共享地址空间。本机代码执行的任何本机内存分配或内存映射都从 Java 运行时中取出内存。
  • 某些 JNI 功能可以将本机内存作为它们普通操作的一部分使用。GetTypeArrayElements和 GetTypeArrayRegion functions可以将 Java 堆数据复制到本机内存缓冲区中,供本机代码使用。是否执行复制将由运行时实现决定;IBM Developer Kit for Java 5.0 及更高版本能执行本机复制。它们做出更改以避免对象受到堆的限制(需要在内存中修复它们。因为 JVM 外部的代码需要引用它);这意味着 Java 堆不能进行分段(但它在 1.4.2 中可以),但是却增加了运行时的本机内存占用。通过复制实现访问大量 Java 堆数据会相应地使用大量本机堆。

NIO

Java 1.4 中增加的新 I/O (NIO) 类引入了一种全新的基于渠道和缓冲区执行 I/O 的方式。再加上 Java 堆中的内存支持的 I/O 缓冲,NIO 添加了对直接ByteBuffer的支持(使用 java.nio.ByteBuffer.allocateDirect()方法分配),它的备份方式是本机内存而不是 Java 堆。直接ByteBuffer可以直接传递给本机 OS 库函数用于执行 I/O —在一些场景中显著提高它们的速度,因为它们可以避免在 Java 堆和本机堆之间复制数据。

直接 ByteBuffers的存储位置很容易让人感到疑惑。应用程序仍然使用 Java 堆上的对象来编制 I/O 操作,但是数据所在的缓冲保存位于本机内存中 —Java 堆对象仅包含到本机堆缓冲的引用。非直接 ByteBuffer将其数据保存在 Java 堆上的一个 byte[]数组中。图 4 显示了直接和非直接 ByteBuffer对象之间的不同:

图 4. 直接和非直接 java.nio.ByteBuffer的内存拓扑
ByteBuffer 内存排列

直接 ByteBuffer对象会自动消除它们的本机缓冲,但是它们只能作为 Java 堆 GC 的一部分 —因此它们不会自动响应本机堆的压力。仅当 Java 堆过满以至于不能响应堆分配请求,或者当 Java 应用程序明确请求它时(不建议这样做,因为会产生性能问题),才会执行 GC。

一种出现问题的情况是本机堆变满且一个或多个直接 ByteBuffers可以胜任 GC 的工作(并且可以释放以增加本机堆的空闲空间),但是 Java 堆基本为空,因此不会执行 GC。

线程

应用程序中的每个线程都需要内存来保存它的 (用于在调用函数时保存本地变量和维持状态的内存区)。根据实现的不同,Java 线程可以有单独的本机和 Java 栈。除了栈空间之外,每个线程都需要一些本机内存用于线程本地存储和内存数据结构。

栈大小因 Java 实现和架构而异。一些实现允许您指定 Java 线程的栈大小。值通常位于 256KB 和 756KB 之间。

虽然每个线程使用的内存量相当少,但对于拥有几百个线程的应用程序,线程栈的内存使用总量会达到很高。当应用程序的线程比可用处理器多时,运行它们的效率会很低,并且会造成性能较差且内存使用增加。

如何知道本机内存已耗尽?

Java 运行时处理 Java 堆耗尽与处理本机堆耗尽的方式不同,但这两种情况都呈现类似的症状。Java 应用程序发现 Java 堆耗尽时很难正常运行 —因为 Java 应用程序若不分配对象则很难执行任何任务。较差的 GC 性能和 OutOfMemoryError表明 Java 堆已填满。

相反,当 Java 运行时启动并且应用程序稳定运行之后,即便本机堆完全耗尽也能继续正常运行。这并不会出示任何古怪的行为,因为需要本机内存的操作比需要 Java 堆分配的操作少很多。虽然需要本机内存的操作因 JVM 实现而异,但也有一些常见的例子:启动线程、加载类以及执行特定类别的网络和文件 I/O。

本机内存不足行为的一致性也比 Java 堆内存不足行为差,因为没有针对本机堆分配的单点控制。但是,所有的 Java 堆分配都在 Java 内存管理系统的控制之下,任何本机代码 —无论在 JVM、Java 类库还是应用程序代码的内部 —都可以执行本机内存分配,并造成它出错。然后,尝试进行分配的代码可以按设计人员的方式来处理它:它可以通过 JNI 接口来抛出一个 OutOfMemoryError,在屏幕上打印输出消息,出现故障并在稍后再次尝试,或执行其他任务。

缺乏可预测行为意味着无法通过一种简单的方式来识别本机内存耗尽。相反,您需要使用来自 OS 和 Java 运行时的数据来确认诊断。

本机内存耗尽示例

为了帮助您了解本机内存耗尽对 Java 运行时的影响,本文的示例代码(参见 下载)包含一些通过不同方式触发本机堆耗尽的 Java 程序。这些示例使用通过 C 编写的本机库占用所有本机进程空间,然后尝试执行一些使用本机内存的操作。提供的示例已经经过生成,但编译它们的指令包含在示例包的顶级目录的 README.html 文件中。

com.ibm.jtc.demos.NativeMemoryGlutton类提供 gobbleMemory()方法,该方法在循环中调用 malloc,直到几乎所有本机内存耗尽。当它完成其任务时,它会打印输出分配给标准错误的字节数,如下所示:

 Allocated 1953546736 bytes of native memory before running out

每个示例的输出都在 32 位 AIX 上运行的 IBM Java 运行时环境中被捕获。示例程序的二进制文件提供在示例包中(参见 下载)。

所使用的 IBM Java 运行时版本如下:

 java version "1.5.0"
 Java(TM) 2 Runtime Environment, Standard Edition (build pap32devifx-20080811c (SR8a)) 
 IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 AIX ppc-32 
 j9vmap3223ifx-20080811 (JIT enabled) 
 J9VM - 20080809_21892_bHdSMr 
 JIT  - 20080620_1845_r8 
 GC   - 200806_19) 
 JCL  - 20080811b

尝试在本机内存耗时启动线程

com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation类尝试在进程地址空间耗尽时启动线程。这是发现 Java 进程内存不足的常用方法,因为许多应用程序都会在它们的生命周期过程中启动线程。

StartingAThreadUnderNativeStarvation的输出如下:

 $ ./run_thread_demo_linux_aix_32.sh 
 Allocated 2652372992 bytes of native memory before running out 
 JVMDUMP006I Processing Dump Event "systhrow", detail "java/lang/OutOfMemoryError 
" - Please Wait. 
 JVMDUMP007I JVM Requesting Snap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/Snap.20081207.105130.487430.0001.trc'
 JVMDUMP010I Snap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/Snap.20081207.105130.487430.0001.trc 
 JVMDUMP007I JVM Requesting Heap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/heapdump.20081207.105130.487430.0002.phd'
 JVMDUMP010I Heap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/heapdump.20081207.105130.487430.0002.phd 
 JVMDUMP007I JVM Requesting Java Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/javacore.20081207.105130.487430.0003.txt'
 JVMDUMP010I Java Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/javacore.20081207.105130.487430.0003.txt 
 JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError"
 . 
 java.lang.OutOfMemoryError: ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.open 
        at java.util.zip.ZipFile.open(Native Method) 
        at java.util.zip.ZipFile.<init>(ZipFile.java:238) 
        at java.util.jar.JarFile.<init>(JarFile.java:169) 
        at java.util.jar.JarFile.<init>(JarFile.java:107) 
        at com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java 
 :69) 
        at com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassL 
 oader.java:113) 
        at java.util.ResourceBundle$1.run(ResourceBundle.java:1111) 
        at java.security.AccessController.doPrivileged(AccessController.java:197 
 ) 
        at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1107) 
        at java.util.ResourceBundle.findBundle(ResourceBundle.java:952) 
        at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:789) 
        at java.util.ResourceBundle.getBundle(ResourceBundle.java:726) 
        at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103) 
        at com.ibm.oti.util.Msg$1.run(Msg.java:44) 
        at java.security.AccessController.doPrivileged(AccessController.java:197 
 ) 
        at com.ibm.oti.util.Msg.<clinit>(Msg.java:41) 
        at java.lang.J9VMInternals.initializeImpl(Native Method) 
        at java.lang.J9VMInternals.initialize(J9VMInternals.java:194) 
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764) 
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758) 
        at java.lang.Thread.uncaughtException(Thread.java:1315) 
 K0319java.lang.OutOfMemoryError: Failed to create a thread: retVal -1073741830, 
 errno 11 
        at java.lang.Thread.startImpl(Native Method) 
        at java.lang.Thread.start(Thread.java:979) 
        at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(StartingA 
 ThreadUnderNativeStarvation.java:33)

调用 java.lang.Thread.start()尝试为新 OS 线程分配内存。尝试失败并造成抛出一个 OutOfMemoryErrorJVMDUMP行通知用户 Java 运行时已经生成了它的标准 OutOfMemoryError调试数据。

尝试处理第一个 OutOfMemoryError会造成第二个错误 —:OutOfMemoryError, ENOMEM error in ZipFile.open。当本机内存耗尽时,经常会出现多个 OutOfMemoryError,因为一些默认 OutOfMemoryError处理例程可能需要分配本机内存。这听起来没有什么作用,但 Java 应用程序抛出的大多数 OutOfMemoryError都是由 Java 堆内存不足造成的,这不会阻止运行时分配本机存储。区分本场景中的 OutOfMemoryError抛出与 Java 堆耗尽造成的其他抛出的惟一方法就是消息。

尝试在本机内存不足时分配直接 ByteBuffer

com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation类尝试在地址空间耗尽时分配一个直接(也就是,本机支持的)java.nio.ByteBuffer对象。它生成以下输出:

 $ ./run_directbytebuffer_demo_aix_32.sh 
 Allocated 2652372992 bytes of native memory before running out 
 JVMDUMP006I Processing Dump Event "systhrow", detail "java/lang/OutOfMemoryError 
" - Please Wait. 
 JVMDUMP007I JVM Requesting Snap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/Snap.20081207.105307.610498.0001.trc'
 JVMDUMP010I Snap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/Snap.20081207.105307.610498.0001.trc 
 JVMDUMP007I JVM Requesting Heap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/heapdump.20081207.105307.610498.0002.phd'
 JVMDUMP010I Heap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/heapdump.20081207.105307.610498.0002.phd 
 JVMDUMP007I JVM Requesting Java Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/javacore.20081207.105307.610498.0003.txt'
 JVMDUMP010I Java Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/javacore.20081207.105307.610498.0003.txt 
 JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError"
 . 
 JVMDUMP006I Processing Dump Event "systhrow", detail "java/lang/OutOfMemoryError 
" - Please Wait. 
 JVMDUMP007I JVM Requesting Snap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/Snap.20081207.105308.610498.0004.trc'
 JVMDUMP010I Snap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/Snap.20081207.105308.610498.0004.trc 
 JVMDUMP007I JVM Requesting Heap Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/heapdump.20081207.105308.610498.0005.phd'
 JVMDUMP010I Heap Dump written to /u2/andhall/aix_samples_pack/OutOfNativeBehavio 
 ur/heapdump.20081207.105308.610498.0005.phd 
 JVMDUMP007I JVM Requesting Java Dump using '/u2/andhall/aix_samples_pack/OutOfNa 
 tiveBehaviour/javacore.20081207.105308.610498.0006.txt'
 UTE430: can't allocate buffer 
 UTE437: Unable to load formatStrings for j9mm 
 UTE430: can't allocate buffer 
 UTE437: Unable to load formatStrings for j9mm 
 UTE430: can't allocate buffer 
 UTE437: Unable to load formatStrings for j9mm 
 UTE430: can't allocate buffer 
 UTE437: Unable to load formatStrings for j9mm 
 UTE430: can't allocate buffer 
 UTE437: Unable to load formatStrings for j9mm 
 JVMDUMP013I Processed Dump Event "systhrow", detail "java/lang/OutOfMemoryError"
 . 
 java.lang.OutOfMemoryError: ZIP006:OutOfMemoryError, ENOMEM error in ZipFile.ope 
 n 
        at java.util.zip.ZipFile.open(Native Method) 
        at java.util.zip.ZipFile.<init>(ZipFile.java:238) 
        at java.util.jar.JarFile.<init>(JarFile.java:169) 
        at java.util.jar.JarFile.<init>(JarFile.java:107) 
        at com.ibm.oti.vm.AbstractClassLoader.fillCache(AbstractClassLoader.java 
 :69) 
        at com.ibm.oti.vm.AbstractClassLoader.getResourceAsStream(AbstractClassL 
 oader.java:113) 
        at java.util.ResourceBundle$1.run(ResourceBundle.java:1111) 
        at java.security.AccessController.doPrivileged(AccessController.java:197 
 ) 
        at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1107) 
        at java.util.ResourceBundle.findBundle(ResourceBundle.java:952) 
        at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:789) 
        at java.util.ResourceBundle.getBundle(ResourceBundle.java:726) 
        at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103) 
        at com.ibm.oti.util.Msg$1.run(Msg.java:44) 
        at java.security.AccessController.doPrivileged(AccessController.java:197) 
        at com.ibm.oti.util.Msg.<clinit>(Msg.java:41) 
        at java.lang.J9VMInternals.initializeImpl(Native Method) 
        at java.lang.J9VMInternals.initialize(J9VMInternals.java:194) 
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:764) 
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:758) 
        at java.lang.Thread.uncaughtException(Thread.java:1315) 
 K0319java.lang.OutOfMemoryError: Unable to allocate 1048576 bytes of 
 direct memory after 5 retries 
        at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:197) 
        at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303) 
        at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main( 
 DirectByteBufferUnderNativeStarvation.java:27) 
 Caused by: java.lang.OutOfMemoryError 
        at sun.misc.Unsafe.allocateMemory(Native Method) 
        at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:184) 
        ... 2 more

在这个场景中,您可以看到 OutOfMemoryError抛出造成的许多 JVMDUMP消息。Java 跟踪引擎生成的一些 UTE错误消息显示它不能分配本机缓冲。这些 UTE错误消息是本机内存不足的一般症状,因为跟踪引擎在默认情况是启动且活动的。最后,打印输出两个 OutOfMemoryErrorzip库中分配第二次失败,以及 java.nio.DirectByteBuffer中的原始错误。

调试方法和技巧

针对 IBM 产品的调试

本文的指导方针是适用于本机内存不足场景的一般调试原则,它们将帮助您调试自己的本机内存问题。如果您要向 IBM 发起问题请求,请始终按照产品的 MustGather 文档收集支持团队所需格式的数据(参见 参考资料)。IBM Guided Activity Assistant —集成在 IBM Support Assistant (ISA) 工作台中的一款工具 —提供了调试若干 IBM 产品问题的最新工作流,这些产品包括 Developer Kit for Java。

遇到 java.lang.OutOfMemoryError或与内存不足相关的错误消息时的第一件事是确定耗尽的是哪种内存。最简单的方法是先检查 Java 堆是否是满的。如果 Java 堆未造成OutOfMemory条件,则您应该分配本机堆的使用。

检查 Java 堆

要检查 Java 堆使用,您可以查看抛出 OutOfMemoryError时生成的 javacore 文件,或使用详细的 GC 数据。javacore 文件通常在 Java 进程的工作目录中生成,并且采用 javacore.<date>.<time>.<pid>.txt 形式的名称。如果您在文本编辑器中打开文件,可以看到如下所示的内容:

 0SECTION       MEMINFO subcomponent dump routine 
 NULL           ================================= 
 1STHEAPFREE    Bytes of Heap Space Free: 416760 
 1STHEAPALLOC   Bytes of Heap Space Allocated: 1344800

本节讨论生成 javacore 时 Java 堆有空闲情况。注意它采用十六进制的值。如果由于无法满足堆分配而抛出 OutOfMemoryError,则 GC 跟踪部分将显示如下内容:

 1STGCHTYPE     GC History  
 3STHSTTYPE     09:59:01:632262775 GMT j9mm.80 -   J9AllocateObject() returning NULL! 
 32 bytes requested for object of class 00147F80

J9AllocateObject() returning NULL!表示对象分配例程已成功完成,并且将抛出一个 OutOfMemoryError

还有可能因为垃圾收集器运行过于频繁而抛出一个 OutOfMemoryError(表示堆已填满,并且 Java 应用程序几乎没有进展)。在这种情况下,您会希望 Heap Space Free 值非常小,并且 GC 历史将显示下面一条消息:

 1STGCHTYPE     GC History  
 3STHSTTYPE     09:59:01:632262775 GMT j9mm.83 -     Forcing J9AllocateObject() 
 to fail due to excessive GC
 1STGCHTYPE     GC History  
 3STHSTTYPE     09:59:01:632262775 GMT j9mm.84 -     Forcing 
 J9AllocateIndexableObject() to fail due to excessive GC

-verbose:gc命令行选项生成包含 GC 统计数据(包括堆占用情况)的跟踪数据。IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV) 工具可以利用此信息显示 Java 堆是否在增加。参见 参考资料,了解如何收集和利用 verbose:gc数据。

测量本机堆使用

如果您已经确定内存不足情况不是由 Java 堆耗尽造成的,则下一个阶段是分配您的本机内存使用。

如果您熟悉 AIX 进程调优,则可以使用喜好的工作链来监控本机进程大小。一种选择是使用 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV) 工具。

GCMV 最初用于分析详细 GC 日志,这允许用户在调优垃圾收集器时查看 Java 堆使用和性能的变化。GCMV 随后得到了扩展,支持分析其他数据源,包括 Linux 和 AIX 本机内存日志。GCMV 作为 ISA 插件随带提供。参见 参考资料,了解如何下载和安装 ISA 及 GCMV,以及如何使用 GCMV 调试 GC 性能问题。

要使用 GCMV 分析 AIX 本机内存配置文件,您必须先使用一个脚本来收集本机内存数据。GCMV 的 AIX 本机内存分析器从 AIX svmon命令读取输出。GCMV 帮助文档中提供了一个脚本,用于收集正确格式的数据。要找到该脚本:

  1. 下载并安装 ISA Version 4(或以上),然后安装 GCMV 工具插件(参见 参考资料了解详细信息)。
  2. 启动 ISA。
  3. 调出 ISA 帮助菜单,方法是单击菜单栏中的 Help >> Help Contents
  4. 在 Tool:IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer >> Using the Garbage Collection and Memory Visualizer >> Supported Data Types >> Native Memory >> AIX Native Memory下方的右侧窗格中找到 AIX 本机内存指令。

图 5 展示了 GCMV 帮助树中包含监控脚本的区域。如果帮助文件在支持数据类型选项卡中没有本机内存区域,那么您需要升级到最新的 GCMV 包。

图 5. GCMV AIX 内存监控脚本在 ISA 帮助对话框中的位置
GCMV 帮助窗口

要使用脚本,将它移动到您的 AIX 机器上,然后启动要监控的 Java 进程。使用 ps获取 Java 进程的进程标识符(PID),然后启动监控脚本(其中,pid是要监控的进程的 ID,output_file是存储内存日志的文件 —GCMV 将要分析的文件):

 sh aix_memory_monitor.sh pid> output_file

要分析内存日志:

  1. 在 ISA 中,从 Launch Activity下拉菜单中选择 Analyze Problem
  2. 在 Analyze Problem面板的顶部选择 Tools选项卡。
  3. 选择 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer
  4. 单击工具面板底部的 Launch按钮。
  5. 单击 Browse 按钮并找到日志文件,单击 OK启动 GCMV。

分析本机内存使用一段时间之后,您需要确定是否出现了本机内存泄漏,还是可用空间的工作太多。即便对于性能良好的 Java 应用程序,本机内存占用在启动之后也不是恒定的。一些 Java 运行时系统 —特别是 JIT 编译器和类加载程序 —的初始化过程需要一段时间,这也会消耗本机内存。初始后的内存增长速度会逐渐增加,但如果在您的场景中,初始内存占用连接地址空间的限制,则这个热身阶段足以造成内存不足。

图 6 显示了一项 Java 压力测试的本机内存占用情况。灰色加亮区显示,在热身阶段中,本机内存占用增加,然后随进程进入稳定状态而趋于平稳。

图 6. AIX 本机内存分析显示热身阶段
热身阶段的 AIX 本机内存分析

还可以让本机内存占用与工作负载相关联。如果您的应用程序创建更多线程来处理传入工作负载,或按比例给应用于系统的负载分配本机支持存储(比如直接 ByteBuffer),那么您可能在高负载下耗尽本机内存。

JVM 热身阶段本机内存的增长以及负载的按比例增长造成的本机内存耗尽是尝试在可用空间内完成过多工作的例子。在这些场景中,您的选择如下:

  • 减少本机内存使用。减少 Java 堆大小是一个很好的选择。
  • 限制本机内存使用。如果您的本机内存增长随负载而变化,则应该通过某种方式限制负载或因此而分配的资源。
  • 增加可用的地址空间量。您可以设置 LDR_CNTRL环境变量来指定一个不同的内存模型配置,或者考虑 迁移到 64 位

真正的本机内存泄漏表现为本机堆持续增加,当负载移除或垃圾收集器运行时仍然不降低。内存泄漏率因负载而异,但泄漏的总内存则不会降低。泄漏的内存不太可能被引用,因此可以将它们换出并维持此状态。

在面对泄漏时,您的选择将受到限制。可以通过 LDR_CNTRL环境变量增加地址空间的总量(因此可泄漏的空间变得更大),但这只能延缓内存耗尽的时间。如果您有足够的物理内存和地址空间,您可以允许泄漏继续,但必须在进程地址空间耗尽之前重新启动应用程序。

谁在使用我的本机内存?

当您确定本机内存耗尽时,下一个逻辑问题是:谁在使用这些内存? AIX 未存储关于哪些代码路径默认分配了特定内存块的信息,但这些信息很容易获得。

在尝试理解本机内存的去处时,第一个步骤是算出您的 Java 设置大约需要多少本机内存。您可以根据以下指导方针大致估算下限:

  • Java 堆占用 -Xmx值。
  • 每个 Java 线程都有一个本机栈和一个 Java 栈。在 AIX 上,每个线程至少需要使用 256KB。
  • 直接 ByteBuffer至少占用提供给 allocate()例程的值。

如果您的总空间小于最大用户空间,那么您的安全性是不确定的。Java 运行时中的许多其他组件都可能会分配足够的内存,从而引起问题;但是,如果您的初始计算表明已经接近最大用户空间,则您很有可能遇到内存问题。如果您怀疑存在本机内存泄漏,或者希望理解内存的准确去向,可以使用一些有用的工具。

AIX 上的许多可用的内存调试器都属于以下类别:

  • 预处理器级别。这些工具需要在测试中通过源编译一个头部。可以使用其中一款工具重新编译您自己的 JNI 库,以跟踪代码中的本机内存泄漏。Dmalloc 是这种工具的一个例子(参见 参考资料)。
  • 链接器级别。这些工具要求测试中的二进制文件与测试中的库重新链接到一起。这对于单独的 JNI 库可行,但不建议用于整个 Java 运行时,因为可能不支持使用经过修改的二进制文件进行运行。Ccmalloc 是这种工具的一个例子(参见 参考资料)。
  • 运行时链接器级别。这些工具使用 LD_PRELOAD环境变量预加载库,用于将标准内存例程替换为插装版本。它们不要求重新编译或者重新链接源代码,但其中许多都不能与 Java 运行时很好地兼容(Linux 等其他操作上可用的工具,比如 NJAMD,也不支持 AIX)。
  • 操作系统级别。AIX 提供了 MALLOCDEBUG工具,用于调试本机内存泄漏。

我建议您阅读 “在 AIX V5.3 中使用 MALLOCDEBUG 隔离并解决内存泄漏” 这篇文章,了解如何使用 MALLOCDEBUG诊断内存泄漏。在这里,我将专注于泄漏 Java 应用程序的输出。您将了解如何使用 MALLOCDEBUG调试遇到本机内存泄漏的 JNI 应用程序。

本文的示例包(参见 下载)包含一个名称为 LeakyJNIApp的 Java 应用程序;它在循环中调用一个会泄漏本机内存的 JNI 方法。默认情况下,它会一直运行直到本机内存耗尽;要让它停止,将运行时间(以秒为单位)作为命令行参数传递给它。

为 malloc调试配置环境,方法是设置 MALLOCDEBUG和 MALLOCTYPE environment variables

 export MALLOCTYPE=debug    
 export MALLOCDEBUG=report_allocations,stack_depth:3

添加 stack_depth:3参数,限制调用 malloc时收集的栈跟踪。JVM 拥有一个惟一的线程栈结构,它会使栈遍历应用程序出现混乱并造成崩溃。通过将栈深度限制为三层,您应该能避免意外行为。

配置好环境之后,运行 LeakyJNIApp10 秒钟,并捕获 stderr输出,其中包含 malloc记录:

 ./run_leaky_jni_app_aix_32.sh 10 2>memory_log.txt

memory_log.txt 文件现在包含泄漏内存块的详细信息:

    Allocation #1175: 0x328B0C00 
        Allocation size: 0x400 
        Allocation traceback: 
        0xD046F4D8  malloc 
        0x32834258  Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 
        0x30CEE7B0  ??

通过查看内存记录文件,您可以确定问题所在。或者,您可以使用 “在 AIX V5.3 中使用 MALLOCDEBUG 隔离并解决内存泄漏” 提供的 format_mallocdebug_op.sh 脚本汇总内存记录。

对 memory_log.txt 文件运行汇总脚本会生成以下输出:

 $ ./format_mallocdebug_op.sh memory_log.txt 
 Parsing output from debug malloc ... 
 Analysed 50 stacks ... 
 Analysed 100 stacks ... 
 Analysed 150 stacks ... 
 Analysed 200 stacks ... 
 Analysed 250 stacks ... 
 Analysed 300 stacks ... 
 Analysed 350 stacks ... 
 Analysed 400 stacks ... 
 Analysed 450 stacks ... 
 Analysed 500 stacks ... 
 Analysed 550 stacks ... 
 Analysed 600 stacks ... 
 Analysed 650 stacks ... 
 Analysed 700 stacks ... 
 Analysed 750 stacks ... 
 Analysed 800 stacks ... 
 Analysed 850 stacks ... 
 Analysed 900 stacks ... 
 Analysed 950 stacks ... 
 Analysed 1000 stacks ... 
 Analysed 1050 stacks ... 
 Analysed 1100 stacks ... 
 Analysed 1150 stacks ... 

 ?? 
 Java_com_ibm_jtc_demos_LeakyJNIApp_nativeMethod 
 malloc 
 ################################ 
 98304 bytes leaked in 96 Blocks 
 ################################ 
 . 
 . 
 .

这显示了来自 LeakyJNIApp.nativeMethod()的泄漏。

一些专用调试应用程序也提供了类似的功能。更多(开源和专用的)工具一直在开发之中,研究目前的最新技术是非常有必要的。

OS 和第三方工具可以让调试更加简单,但是它们消除了合理调试技巧的需求。一些建议的步骤如下:

  • 提取测试用例。生成一个可以重新引起本机泄漏的独立环境。这可以让调试更加简单。
  • 尽可以缩减测试用例。尝试删除一些功能以确定哪些代码路径在造成本机泄漏。如果您有自己的 JNI 库,尝试完全禁用它们,以确定它们是否会造成泄漏。
  • 减小 Java 堆大小。Java 堆可能是进程中最大的虚拟地址空间使用者。通过减小 Java 堆,您为本机内存的其他使用者提供了更多可用的空间,这样可以让应用程序运行更长时间。
  • 关联本机进程大小。了解本机内存在一段时间内的使用情况之后,您可以将它与应用程序负载以及 GC 数据进行比较。如果泄漏率与负载水平成正比,则表明泄漏是由各事务或操作上的某个因素造成的。如果本机进程大小在执行 GC 时显著降低,则表明您未遇到内存泄漏 —而是本机支持造成的对象累积(比如直接 ByteBuffer)。您可以减少本机支持对象占用的内存量,方法是减少 Java 堆大小(因此强制收集更加频繁地执行)或自己在对象缓存中(而不是依靠垃圾收集来清理它们)管理它们。

如果您认为泄漏或内存增长是由 Java 运行时本身造成时,您会希望运行时供应商参与进一步调试。

消除限制:转换成 64 位

在 32 位 Java 运行时中,很容易出现本机内存不足的情况,因为地址空间相对较小。32 位操作系统提供的 2 到 4GB 用户空间经常小于系统附带的物理内存总量,并且现代的数据密集型应用程序在扩展后能很容易地填满可用空间。

如果您的应用程序不能适应 32 位地址空间,那么您可以通过迁移到 64 位 Java 运行时来获取更多用户空间。如果您可以在 AIX 上运行 64 位 Java 运行时,则 448PB 的地址空间提供了非常大的 Java 堆,并能减少与地址空间相关的问题。

但是,迁移到 64 位并不是适用于所有本机内存问题的通用解决方案;您仍然需要足够的物理内存来保存所有数据。如果您的 Java 运行时不能完全加载到物理内存中,则性能会差到极点,因为操作系统需要与交换空间来回复制 Java 运行时数据。基于此原因,迁移到 64 位并不是针对内存泄漏的万能解决方案 —您只是增加了可以泄漏的空间,这只是延长了应用程序正常运行的时间。

不能在 64 位运行时中使用 32 位本机代码;任何本机代码(JNI 库、JVM Tool Interface [JVMTI]、JVM Profiling Interface [JVMPI] 和 JVM Debug Interface [JVMDI] 代理)都必须针对 64 位进行重新编译。在相同的硬件上,64 位运行时的性能有时还会比相应的 32 位运行时慢。64 位运行时使用 64 位指针(本机地址引用),因此 64 位运行时中的相同对象会占用更多的空间。更大的对象意味着用更大的堆来保存相同大小的数据,同时维持类似的 GC 性能,这会拖慢操作系统和硬件内存系统。令人惊讶的是,更大的 Java 堆并不一定意味着更长的 GC 暂停时间,因为暂停时间主要由堆中的活动数据决定(这些数据可能未增加),并且一些 GC 算法对于更大的堆效率更高。

虽然 64 位运行时的性能历来比相应的 32 位运行时低,但这种情况在 IBM Developer Kit for Java 6.0 中得到了显著改善。此外,压缩引用技术(通过 -Xcompressedrefs命令行参数启用)允许您在执行 32 位对象寻址时使用大 Java 堆(在 Service Refresh 2 上最大可达到 20-30GB)。这样可以清除造成之前 64 位运行时运行过慢的 “对象膨胀”。

Java 运行时的性能对比不在本文的讨论范围之内 —但如果您考虑迁移到 64 位运行时,则有必要先在 64 位运行时中测试您的应用程序,并使用 IBM Developer Kit for Java 6 利用压缩引用的优势。

结束语

在设计和运行大型 Java 应用程序时,理解本机内存至关重要,但是这一点通常被忽略,因为它与复杂的硬件和操作系统细节密切相关,Java 运行时的目的正是帮助我们规避这些细节。JRE 是一个本机进程,它必须在由这些纷繁复杂的细节定义的环境中工作。要从 Java 应用程序中获得最佳的性能,您必须理解应用程序如何影响 Java 运行时的本机内存使用。

耗尽本机内存与耗尽 Java 堆很相似,但它需要不同的工具集来调试和解决。修复本机内存问题的关键在于理解运行您的 Java 应用程序的硬件和操作系统施加的限制,并将其与操作系统工具知识结合起来,监控本机内存使用。通过这种方法,您将能够解决 Java 应用程序产生的一些非常棘手的问题。

下载

描述 名字 大小
本机内存示例代码 j-nativememory-aix.zip 33KB
Javacore 内存分析脚本 j-nativememory-aix2.zip 3KB

参考资料

学习

获得产品和技术

条评论


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值