读书人

JAVA 内存详解 (理解 JVM 怎么使用 W

发布时间: 2012-07-18 12:05:40 作者: rapoo

JAVA 内存详解 (理解 JVM 如何使用 Windows 和 Linux 上的本机内存)

级别: 中级

Andrew Hall?, 软件工程师, IBM

2009 年 5 月 11 日

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

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

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

本系列文章共分两篇,讨论不同平台上的相应话题。本文是其中一篇。在这两篇文章中,您将了解什么是本机内存,Java 运行时如何使用它,本机内存耗尽之后会发生什么情况,以及如何调试本机?OutOfMemoryError?。本文介绍 Windows 和 Linux 平台上的这一主题,不会介绍任何特定的运行时实现。另一篇?类似的文章?介绍 AIX 上的这一主题,着重介绍 IBM? Developer Kit for Java。(另一篇文章中关于 IBM 实现的信息也适合于除 AIX 之外的平台,因此如果您在 Linux 上使用 IBM Developer Kit for Java,或使用 IBM 32-bit Runtime Environment for Windows,您会发现这篇文章也有用处)。

?

程序的每个实例以进程?的形式运行。在 Linux 和 Windows 上,进程是一个由受操作系统控制的资源(比如文件和套接字信息)、一个典型的虚拟地址空间(在某些架构上不止一个)和至少一个执行线程构成的集合。

虚 拟地址空间大小可能比处理器的物理地址大小更小。32 位 Intel x86 最初拥有的 32 位物理地址仅允许处理器寻址 4GB 存储空间。后来,添加了一种称为物理地址扩展(Physical Address Extension,PAE)的特性,将物理地址大小扩大到了 36 位,允许安装或寻址至多 64GB RAM。PAE 允许操作系统将 32 位的 4GB 虚拟地址空间映射到一个较大的物理地址范围,但是它不允许每个进程拥有 64GB 虚拟地址空间。这意味着如果您将大于 4GB 的内存放入 32 位 Intel 服务器中,您将无法将所有内存直接映射到一个单一进程中。

地址窗口扩展(Address Windowing Extension)特性允许 Windows 进程将其 32 位地址空间的一部分作为滑动窗口映射到较大的内存区域中。Linux 使用类似的技术将内存区域映射到虚拟地址空间中。这意味着尽管您无法直接引用大于 4GB 的内存,但您仍然可以使用较大的内存区域。

?

图 3 显示了 32 位 Linux 的地址-空间配置:


图 3. 32 位 Linux 的地址-空间布局?
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?

31 位 Linux 390 上还使用了一个独立的内核地址空间,其中较小的 2GB 地址空间使对单个地址空间进行划分不太合理,但是,390 架构可以同时使用多个地址空间,而且不会降低性能。

进 程空间必须包含程序需要的所有内容,包括程序本身和它使用的共享库(在 Windows 上为 DDL,在 Linux 上为 .so 文件)。共享库不仅会占据空间,使程序无法在其中存储数据,它们还会使地址空间碎片化,减少可作为连续内存块分配的内存。这对于在拥有 3GB 用户空间的 Windows x86 上运行的程序尤为明显。DLL 在构建时设置了首选的加载地址:当加载 DLL 时,它被映射到处于特定位置的地址空间,除非该位置已经被占用,在这种情况下,它会加载到别处。Windows NT 最初设计时设置了 2GB 可用用户空间,这对于要构建来加载接近 2GB 区域的系统库很有用 —— 使大部分用户区域都可供应用程序自由使用。当用户区域扩展到 3GB 时,系统共享库仍然加载接近 2GB 数据(约为用户空间的一半)。尽管总体用户空间为 3GB,但是不可能分配 3GB 大的内存块,因为共享库无法加载这么大的内存。

在 Windows 中使用?/3GB?开关,可以将内核空间减少一半,也就是最初设计的大小。在一些情形下,可能耗尽 1GB 内核空间,使 I/O 变得缓慢,且无法正常创建新的用户会话。尽管?/3GB?开关可能对一些应用程序非常有用,但任何使用它的环境在部署之前都应该进行彻底的负载测试。参见?参考资料?,获取关于?/3GB?开关及其优缺点的更多信息的链接。

本 机内存泄漏或过度使用本机内存将导致不同的问题,具体取决于您是耗尽了地址空间还是用完了物理内存。耗尽地址空间通常只会发生在 32 位进程上,因为最大 4GB 的内存很容易分配完。64 位进程具有数百或数千 GB 的用户空间,即使您特意消耗空间也很难耗尽这么大的空间。如果您确实耗尽了 Java 进程的地址空间,那么 Java 运行时可能会出现一些陌生现象,本文稍后将详细讨论。当在进程地址空间比物理内存大的系统上运行时,内存泄漏或过度使用本机内存会迫使操作系统交换后备存 储器来用作本机进程的虚拟地址空间。访问经过交换的内存地址比读取驻留(在物理内存中)的地址慢得多,因为操作系统必须从硬盘驱动器拉取数据。可能会分配 大量内存来用完所有物理内存和所有交换内存(页面空间),在 Linux 上,这将触发内核内存不足(OOM)结束程序,强制结束最消耗内存的进程。在 Windows 上,与地址空间被占满时一样,内存分配将会失败。

同时,如果尝试使用比物理内存大的虚拟内存,显然在进程由于消 耗内存太大而被结束之前就会遇到问题。系统将变得异常缓慢,因为它会将大部分时间用于在内存与交换空间之间来回复制数据。当发生这种情况时,计算机和独立 应用程序的性能将变得非常糟糕,从而使用户意识到出现了问题。当 JVM 的 Java 堆被交换出来时,垃圾收集器的性能会变得非常差,应用程序可能被挂起。如果一台机器上同时使用了多个 Java 运行时,那么物理内存必须足够分配给所有 Java 堆。

?

JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首


?

直接?ByteBuffer?对象会自动清理本机缓冲区,但这个过程只能作为 Java 堆 GC 的一部分来执行,因此它们不会自动响应施加在本机堆上的压力。GC 仅在 Java 堆被填满,以至于无法为堆分配请求提供服务时发生,或者在 Java 应用程序中显式请求它发生(不建议采用这种方式,因为这可能导致性能问题)。

发生垃圾收集的情形可能是,本机堆被填满,并且一个或多个直接?ByteBuffers?适合于垃圾收集(并且可以被释放来腾出本机堆的空间),但 Java 堆几乎总是空的,所以不会发生垃圾收集。


JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首



JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首


?

调用?java.lang.Thread.start()?来尝试为一个新的操作系统线程分配内存。此尝试会失败并抛出OutOfMemoryError?。JVMDUMP?行通知用户 Java 运行时已经生成了标准的?OutOfMemoryError?调试数据。

尝试处理第一个?OutOfMemoryError?会导致第二个错误 ——?:OutOfMemoryError, ENOMEM error in ZipFile.open?。当本机进程内存耗尽时通常会抛出多个?OutOfMemoryError?。Failed to fork OS thread?可能是在耗尽本机内存时最常见的消息。

本文提供的示例会触发一个?OutOfMemoryError?集群,这比您在自己的应用程序中看到的情况要严重得多。这一定程度上是因为几乎所有本机内存都已被使用,与实际的应用程序不同,使用的内存不会在以后被释放。在实际应用程序中,当抛出?OutOfMemoryError?时,线程会关闭,并且可能会释放一部分本机内存,以让运行时处理错误。测试案例的这个细微特性还意味着,类库的许多部分(比如安全系统)未被初始化,而且 它们的初始化受尝试处理内存耗尽情形的运行时驱动。在实际应用程序中,您可能会看到显示了很多错误,但您不太可能在一个位置看到所有这些错误。

在 Sun Java 运行时上执行相同的测试案例时,会生成以下控制台输出:

?

尽管堆栈轨迹和错误消息稍有不同,但其行为在本质上是一样的:本机分配失败并抛出java.lang.OutOfMemoryError?。此场景中抛出的?OutOfMemoryError?与由于 Java 堆耗尽而抛出的错误的惟一区别在于消息。

?

在此场景中,抛出了?OutOfMemoryError?,它会触发默认的错误文档。OutOfMemoryError?到达主线程堆栈的顶部,并在?stderr?上输出。

当在 Sun Java 运行时上运行时,此测试案例生成以下控制台输出:


JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首


查阅供应商文档?

本 文提供的指南是一般的调试原则,可用于理解本机内存耗尽场景。您的运行时供应商可能提供了自己的调试说明,供应商期望您按照这些说明与其支持团队联系。如 果您要与运行时供应商(包括 IBM)合作解决问题,请始终检查其调试和诊断文档,查看在提交问题报告时应该执行哪些步骤。

当出现?java.lang.OutOfMemoryError?或看到有关内存不足的错误消息时,要做的第一件事是确定哪种类型的内存被耗尽。最简单的方式是首先检查 Java 堆是否被填满。如果 Java 堆未导致OutOfMemory?条件,那么您应该分析本机堆使用情况。

?

IBM 和 Sun 实现都拥有一个详细的 GC 选项,用于在每个 GC 周期生成显示堆填充情况的跟踪数据。此信息可使用工具(比如 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV))来分析,以显示 Java 堆是否在增长(参见?参考资料?)。

?

GCMV 帮助文件中提供的脚本使用的?ps?命令仅适用于最新的?ps?版本。在一些旧的 Linux 分发版中,帮助文件中的命令将会生成错误信息。要查看您的 Linux 分发版上的行为,可以尝试运行?ps -o pid,vsz=VSZ,rss=RSS。如果您的?ps?版本支持新的命令行参数语法,那么得到的输出将类似于:

?

本机内存占用也可能应工作负载不同而异。如果您的应用程序创建了较多进程来处理传入的工作负载,或者根据应用于系统的负载量按比例分配本机存储(比如直接?ByteBuffer?),则可能由于负载过高而耗尽本机内存。

由于 JVM 前期阶段的本机内存增长而耗尽本机内存,以及内存使用随负载增加而增加,这些都是尝试在可用空间中做太多事情的例子。在这些场景中,您的选择是:

减少本机内存使用。?缩小 Java 堆大小是一个好的开端。限制本机内存使用。?如果您的本机内存随负载增加而增加,可以采取某种方式限制负载或为负载分配的资源。增加可用地址空间。?这可以通过以下方式实现:调优您的操作系统(例如,在 Windows 上使用?/3GB开关增加用户空间,或者在 Linux 上使用庞大的内核空间),更换平台(Linux 通常拥有比 Windows 更多的用户空间),或者?转移到 64 位操作系统?。

一种实际的本机内存泄漏表现为本机堆的持续增长,这些内存不会在移除负载或运行垃圾收集器时减少。内存泄漏程度因负载不同而不同,但泄漏的总内存不会下降。泄漏的内存不可能被引用,因此它可以被交换出去,并保持被交换出去的状态。

当遇到内存泄漏时,您的选择很有限。您可以增加用户空间(这样就会有更多的空间供泄漏),但这仅能延缓最终的内存耗尽。如果您拥有足够的物理内存和地址空间,并且会在进程地址空间耗尽之前重启应用程序,那么可以允许地址空间继续泄漏。


JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首



JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)
JAVA 内存详解 (理解 JVM 怎么使用 Windows 和 Linux 上的本机内存)?回页首


结束语

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

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

读书人网 >UNIXLINUX

热点推荐