客户服务***: 4008-114-118
找招聘会
找资讯信息
选择搜索范围
公司名 当前位置:
JVM JVM
来源:互动百科
发布日期: 2009-01-20
JVM(Java虚拟机)一种用于计算设备的规范,可用不同的方式(软件或硬件)加以实现。编译虚拟机的指令集与编译微处理器的指令集非常类似。Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
Java虚拟机(JVM)是可运行Java代码的假想计算机。只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行。Java虚拟机是一个想象中的机器,在实际的计算机上通过软件模拟来实现。Java虚拟机有自己想象中的硬件,如处理器、堆栈、寄存器等,还具有相应的指令系统。
基本概述
数据类型
规格描述
指令系统
基本概述
数据类型
规格描述
指令系统
碎片回收堆
体系结构
局部变量
异常捕捉
操作数栈区
运行过程
相关词条
参考资料
[显示全部]
编辑本段
JVM - 基本概述
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。
Java虚拟机是Java语言底层实现的基础,对Java语言感兴趣的人都应对Java虚拟机有个大概的了解。这有助于理解Java语言的一些性质,也有助于使用Java语言。对于要在特定平台上实现Java虚拟机的软件人员,Java语言的编译器作者以及要用硬件芯片实现Java虚拟机的人来说,则必须深刻理解Java虚拟机的规范。另外,如果你想扩展Java语言,或是把其它语言编译成Java语言的字节码,你也需要深入地了解Java虚拟机。
编辑本段
JVM - 数据类型
Java虚拟机支持Java语言的基本数据类型如下:
byte://1字节有符号整数的补码
short://2字节有符号整数的补码
int://4字节有符号整数的补码
long://8字节有符号整数的补码
float://4字节IEEE754单精度浮点数
double://8字节IEEE754双精度浮点数
char://2字节无符号Unicode字符
Java类型检查都是在编译时完成的。上面列出的原始数据类型的数据在Java执行时不需要用硬件标记。操作这些原始数据类型数据的字节码(指令)本身就已经指出了操作数的数据类型,例如iadd、ladd、fadd和dadd指令都是把两个数相加,其操作数类型别是int、long、float和double。虚拟机没有给boolean(布尔)类型设置单独的指令。boolean型的数据是由integer指令,包括integer返回来处理的。boolean型的数组则是用byte数组来处理的。虚拟机使用IEEE754格式的浮点数。不支持IEEE格式的较旧的计算机,在运行Java数值计算程序时,可能会非常慢。
虚拟机支持的其它数据类型包括:
object//对一个Javaobject(对象)的4字节引用
returnAddre //4字节,用于jsr/ret/jsr-w/ret-w指令
注:Java数组被当作object处理。
虚拟机的规范对于object内部的结构没有任何特殊的要求。在Sun公司的实现中,对object的引用是一个句柄,其中包含一对指针:一个指针指向该object的方法表,另一个指向该object的数据。用Java虚拟机的字节码表示的程序应该遵守类型规定。Java虚拟机的实现应拒绝执行违反了类型规定的字节码程序。Java虚拟机由于字节码定义的限制似乎只能运行于32位地址空间的机器上。但是可以创建一个Java虚拟机,它自动地把字节码转换成64位的形式。从Java虚拟机支持的数据类型可以看出,Java对数据类型的内部格式进行了严格规定,这样使得各种Java虚拟机的实现对数据的解释是相同的,从而保证了Java的与平台无关性和可
移植性。
编辑本段
JVM - 规格描述
JVM的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提范的任何系统上运行。JVM对其实现的某些方面给出了具体的定义,特别是对Java可执行代码,即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码和操作数的语法和数值、标识符的数值表示方式、以及Java类文件中的Java对象、常量缓冲池在JVM的存储映象。这些定义为JVM解释器开发人员提供了所需的信息和开发环境。Java的设计者希望给开发人员以随心所欲使用Java的自由。
JVM定义了控制Java代码解释执行和具体实现的五种规格,它们是:
JVM指令系统
JVM寄存器
JVM栈结构
JVM碎片回收堆
JVM存储区
编辑本段
JVM - 指令系统
JVM指令系统同其他计算机的指令系统极其相似。Java指令也是由 操作码和操作数两部分组成。操作码为8位二进制数,操作数进紧随在操作码的后面,其长度根据需要而不同。操作码用于指定一条指令操作的性质(在这里采用汇编符号的形式进行说明),如iload表示从存储器中装入一个整数,anewarray表示为一个新数组分配空间,iand表示两个整数的“与”,ret用于流程控制,表示从对某一方法的调用中返回。当长度大于8位时,操作数被分为两个以上字节存放。JVM采用了"big endian"的编码方式来处理这种情况,即高位bits存放在低字节中。这同 Motorola及其他的RISC CPU采用的编码方式是一致的,而与Intel采用的“little endian”的编码方式即低位bits存放在低位字节的方法不同。
Java指令系统是以Java语言的实现为目的设计的,其中包含了用于调用方法和监视多先程系统的指令。Java的8位操作码的长度使得JVM最多有256种指令,已使用了160多种操作码。
所有的CPU均包含用于保存系统状态和处理器所需信息的寄存器组。如果虚拟机定义较多的寄存器,便可以从中得到更多的信息而不必对栈或内存进行访问,这有利于提高运行速度。然而,如果虚拟机中的寄存器比实际CPU的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器,这反而会降低虚拟机的效率。针对这种情况,JVM只设置了4个最为常用的寄存器。它们是:
pc程序计数器
optop操作数栈顶指针
frame当前执行环境指针
vars指向当前执行环境中第一个局部变量的指针所有寄存器均为32位。pc用于记录程序的执行。optop,frame和vars用于记录指向Java栈区的指针。
编辑本段
JVM - 栈结构
作为基于栈结构的计算机,Java栈是JVM存储信息的主要方法。当JVM得到一个Java字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。每个栈框架包括以下三类信息:
局部变量
执行环境
操作数栈
局部变量用于存储一个类的方法中所用到的局部变量。vars寄存器指向该变量表中的第一个局部变量。
执行环境用于保存解释器对Java字节码进行解释过程中所需的信息。它们是:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。执行环境是一个执行一个方法的控制中心。例如:如果解释器要执行iadd(整数加法),首先要从frame寄存器中找到当前执行环境,而后便从执行环境中找到操作数栈,从栈顶弹出两个整数进行加法运算,最后将结果压入栈顶。
操作数栈用于存储运算所需操作数及运算的结果。
编辑本段
JVM - 碎片回收堆
Java类的实例所需的存储空间是在堆上分配的。解释器具体承担为类实例分配空间的工作。解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。一旦对象使用完毕,便将其回收到堆中。
在Java语言中,除了new语句外没有其他方法为一对象申请和释放内存。对内存进行释放和回收的工作是由Java运行系统承担的。这允许Java运行系统的设计者自己决定碎片回收的方法。在SUN公司开发的Java解释器和Hot Java环境中,碎片回收用后台线程的方式来执行。这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。
JVM有两类存储区:常量缓冲池和方法区。常量缓冲池用于存储类名称、方法和字段名称以及串常量。方法区则用于存储Java方法的字节码。对于这两种存储区域具体实现方式在JVM规格中没有明确规定。这使得Java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。
JVM是为Java字节码定义的一种独立于具体平台的规格描述,是Java平台独立性的基础。JVM还存在一些限制和不足,有待于进一步的完善,但无论如何,JVM的思想是成功的。
对比分析:如果把Java原程序想象成C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。
Java解释器相当于运行Java字节码的“CPU”,但该“CPU”不是通过硬件实现的,而是用软件实现的。Java解释器实际上就是特定的平台下的一个应用程序。只要实现了特定平台下的解释器程序,Java字节码就能通过解释器程序在该平台下运行,这是Java跨平台的根本。当前,并不是在所有的平台下都有相应Java解释器程序,这也是Java并不能在所有的平台下都能运行的原因,它只能在已实现了Java解释器程序的平台下运行。
编辑本段
JVM - 体系结构
JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些??归功于设计JVM时的体系结构了。一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分。
JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。
执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。
Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。
虚拟机的内层循环的执行过程如下:
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)
由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:
第一个字节*256+第二个字节字节码。
指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。
Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。
Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java虚拟机规范。
Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:
pc: Java程序计数器;
optop: 指向操作数栈顶端的指针;
frame: 指向当前执行方法的执行环境的指针;
vars: 指向当前执行方法的局部变量区第一个变量的指针。
在上述体系结构图中所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。
Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
编辑本段
JVM - 局部变量
每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。
运行环境区:在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。
动态链接:运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的cla 文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。
正常的方法返回:如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。
编辑本段
JVM - 异常捕捉
异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因运行时错,如对一个空指针的引用。程序使用了throw语句。
当异常发生时,Java虚拟机采取如下措施:
1、检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。
2、与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如果找到了匹配的catch子句,那么系统转移到指定的异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所有嵌套的catch子句都被检查过。
3、由于虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。
4、如果找不到匹配的catch子句,那么当前方法得到一个“未截获异常”的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。
编辑本段
JVM - 操作数栈区
机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。
编辑本段
JVM - 运行过程
对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:
cla HelloA {
public static void main(String[] args)
System.out.println("Hello World!");
for (int i = 0; i args.length; i++ )
System.out.println(args);
编译后在命令行模式下键入: java HelloA run virtual machine 将通过调用HelloA 的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。略述虚拟机在执行HelloA 时可能采取的步骤。
开始试图执行类HelloA 的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用Cla Loader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloA 与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。
编辑本段
JVM - 相关词条
编辑本段
JVM - 参考资料
1、http://www.itisedu.com/phrase/200604261007235.html
2、http://www.wujianrong.com/archives/2006/11/jvm.html
3、http://www.it315.org/download/JavaVM/JVM.htm
把相关招聘信息分享到:
文章关键词:
相关信息频道:
上一篇:
下一篇:
企业***总机: 010-59646999 57930055
周末/假期企业服务值班***: 010-59646999
企业***直线: 010-84450639 84450561 84450633 84956436 84926406 84450630 84450591 84450627
企业***手机:胡毅 13488870336
张圣会 15300088541
企业***QQ: 1357082238
1274269085
1311942924
1339784037
1565785596
个人***邮件联系: 版权所有:
子站分类: Copyright 2003-2011如果您还没有注册到 IBM 注册系统,我们为给您带来的不便表示道歉,并请您马上注册。
IBM ID:
密码:
登录之后:
留在当前页面
My developerWorks 概要信息
My developerWorks 首页
保持登录。
单击提交则表示您同意developerWorks 的条款和条件。
当您初次登录到 developerWorks 时,将会为您创建一份概要信息,概要信息中包括您的姓名和您在注册 developerWorks 时选择的昵称。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容显示在一起。
所有提交的信息确保安全。
当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。
昵称长度在 3 至 31 个字符之间
。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。
昵称:
则表示您同意developerWorks 的条款和条件。
所有提交的信息确保安全。
My developerWorks:
我的通知:
选择语言:
IBM 产品:
技术:
解决方案:
查找软件:
内存详解
理解 JVM 如何使用 Windows 和 Linux 上的本机内存
, 软件工程师, IBM
Andrew Hall 于 2004 年加入 IBM Java Technology Centre,他在 Java System Test 小组工作了两年。然后在 Java 服务团队工作了 18 个月,其间,他在多个平台上调试了数十个本机内存问题。他目前是 Java Reliability, Availability and Serviceability 团队的成员。在业余生活中,他喜欢阅读、摄影和玩魔术。
简介: Java™ 堆耗尽并不是造成
java.lang.OutOfMemoryError
的惟一原因。如果
本机内存
耗尽,则会发生普通调试技巧无法解决的
OutOfMemoryError
。本文将讨论本机内存的概念,Java 运行时如何使用它,它被耗尽时会出现什么情况,以及如何在 Window #174; 和 Linux® 上调试本机
OutOfMemoryError
。针对 AIX® 系统的相同主题将在
中介绍。
发布日期: 2009 年 5 月 11 日
级别: 中级
其他语言版本: 访问情况 7037 次浏览
建议: 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,您会发现这篇文章也有用处)。
我将首先解释一下操作系统和底层硬件给本机内存带来的限制。如果您熟悉使用 C 等语言管理动态内存,那么您可以直接跳到
本机进程遇到的许多限制都是由硬件造成的,而与操作系统没有关系。每台计算机都有一个处理器和一些随机存取存储器(RAM),后者也称为物理内存。处理器将数据流解释为要执行的指令,它拥有一个或多个处理单元,用于执行整数和浮点运算以及更高级的计算。处理器具有许多
寄存器
—— 常快速的内存元素,用作被执行的计算的工作存储,寄存器大小决定了一次计算可使用的最大数值。
处理器通过内存总线连接到物理内存。物理地址(处理器用于索引物理 RAM 的地址)的大小限制了可以寻址的内存。例如,一个 16 位物理地址可以寻址 0x0000 到 0xFFFF 的内存地址,这个地址范围包括 2^16 = 65536 个惟一的内存位置。如果每个地址引用一个存储字节,那么一个 16 位物理地址将允许处理器寻址 64KB 内存。
处理器被描述为特定数量的数据位。这通常指的是寄存器大小,但是也存在例外,比如 32 位 390 指的是物理地址大小。对于桌面和服务器平台,这个数字为 31、32 或 64;对于嵌入式设备和微处理器,这个数字可能小至 4。物理地址大小可以与寄存器带宽一样大,也可以比它大或小。如果在适当的操作系统上运行,大部分 64 位处理器可以运行 32 位程序。
表 1 列出了一些流行的 Linux 和 Windows 架构,以及它们的寄存器和物理地址大小:
寄存器带宽(位)
物理地址大小(位)
(现代)Intel® x86
36,具有物理地址扩展(Pentium Pro 和更高型号)
目前为 48 位(以后将会增大)
在 POWER 5 上为 50 位
390 31 位
390 64 位
如果您编写无需操作系统,直接在处理器上运行的应用程序,您可以使用处理器可以寻址的所有内存(假设连接到了足够的物理 RAM)。但是要使用多任务和硬件抽象等特性,几乎所有人都会使用某种类型的操作系统来运行他们的程序。
在 Windows 和 Linux 等多任务操作系统中,有多个程序在使用系统资源。需要为每个程序分配物理内存区域来在其中运行。可以设计这样一个操作系统:每个程序直接使用物理内存,并且可以可靠地仅使用分配给它的内存。一些嵌入式操作系统以这种方式工作,但是这在包含多个未经过集中测试的应用程序的环境中是不切实际的,因为任何程序都可能破坏其他程序或者操作系统本身的内存。
虚拟内存
允许多个进程共享物理内存,而且不会破坏彼此的数据。在具有虚拟内存的操作系统(比如 Windows、Linux 和许多其他操作系统)中,每个程序都拥有自己的虚拟地址空间 —— 一个逻辑地址区域,其大小由该系统上的地址大小规定(所以,桌面和服务器平台的虚拟地址空间为 31、32 或 64 位)。进程的虚拟地址空间中的区域可被映射到物理内存、文件或任何其他可寻址存储。当数据未使用时,操作系统可以在物理内存与一个交换区域(Windows 上的
页面文件
或者 Linux 上的
交换分区
)之间移动它,以实现对物理内存的最佳利用率。当一个程序尝试使用虚拟地址访问内存时,操作系统连同片上硬件会将该虚拟地址映射到物理位置,这个位置可以是物理 RAM、一个文件或页面文件/交换分区。如果一个内存区域被移动到交换空间,那么它将在被使用之前加载回物理内存中。图 1 展示了虚拟内存如何将进程地址空间区域映射到共享资源:
程序的每个实例以
的形式运行。在 Linux 和 Windows 上,进程是一个由受操作系统控制的资源(比如文件和套接字信息)、一个典型的虚拟地址空间(在某些架构上不止一个)和至少一个执行线程构成的集合。
虚拟地址空间大小可能比处理器的物理地址大小更小。32 位 Intel x86 最初拥有的 32 位物理地址仅允许处理器寻址 4GB 存储空间。后来,添加了一种称为物理地址扩展(Physical Addre Exte ion,PAE)的特性,将物理地址大小扩大到了 36 位,允许***或寻址至多 64GB RAM。PAE 允许操作系统将 32 位的 4GB 虚拟地址空间映射到一个较大的物理地址范围,但是它不允许每个进程拥有 64GB 虚拟地址空间。这意味着如果您将大于 4GB 的内存放入 32 位 Intel 服务器中,您将无法将所有内存直接映射到一个单一进程中。
地址窗口扩展(Addre Windowing Exte ion)特性允许 Windows 进程将其 32 位地址空间的一部分作为滑动窗口映射到较大的内存区域中。Linux 使用类似的技术将内存区域映射到虚拟地址空间中。这意味着尽管您无法直接引用大于 4GB 的内存,但您仍然可以使用较大的内存区域。
尽管每个进程都有其自己的地址空间,但程序通常无法使用所有这些空间。地址空间被划分为
用户空间
内核空间
。内核是主要的操作系统程序,包含用于连接计算机硬件、调度程序以及提供联网和虚拟内存等服务的逻辑。
作为计算机启动序列的一部分,操作系统内核运行并初始化硬件。一旦内核配置了硬件及其自己的内部状态,第一个用户空间进程就会启动。如果用户程序需要来自操作系统的服务,它可以执行一种称为
系统调用
的操作与内核程序交互,内核程序然后执行该请求。系统调用通常是读取和写入文件、联网和启动新进程等操作所必需的。
当执行系统调用时,内核需要访问其自己的内存和调用进程的内存。因为正在执行当前线程的处理器被配置为使用地址空间映射来为当前进程映射虚拟地址,所以大部分操作系统将每个进程地址空间的一部分映射到一个通用的内核内存区域。被映射来供内核使用的地址空间部分称为内核空间,其余部分称为用户空间,可供用户应用程序使用。
内核空间和用户空间之间的平衡关系因操作系统的不同而不同,甚至在运行于不同硬件架构之上的同一操作系统的各个实例间也有所不同。这种平衡通常是可配置的,可进行调整来为用户应用程序或内核提供更多空间。缩减内核区域可能导致一些问题,比如能够同时登录的用户数量限制或能够运行的进程数量限制。更小的用户空间意味着应用程序编程人员只能使用更少的内存空间。
默认情况下,32 位 Windows 拥有 2GB 用户空间和 2GB 内核空间。在一些 Windows 版本上,通过向启动配置添加
开关并使用
/LARGEADDRESSAWARE
开关重新链接应用程序,可以将这种平衡调整为 3GB 用户空间和 1GB 内核空间。在 32 位 Linux 上,默认设置为 3GB 用户空间和 1GB 内核空间。一些 Linux 分发版提供了一个
hugemem
内核,支持 4GB 用户空间。为了实现这种配置,将进行系统调用时使用的地址空间分配给内核。通过这种方式增加用户空间会减慢系统调用,因为每次进行系统调用时,操作系统必须在地址空间之间复制数据并重置进程地址-空间映射。图 2 展示了 32 位 Windows 的地址-空间布局:
图 3 显示了 32 位 Linux 的地址-空间配置:
31 位 Linux 390 上还使用了一个独立的内核地址空间,其中较小的 2GB 地址空间使对单个地址空间进行划分不太合理,但是,390 架构可以同时使用多个地址空间,而且不会降低性能。
进程空间必须包含程序需要的所有内容,包括程序本身和它使用的共享库(在 Windows 上为 DDL,在 Linux 上为 .so 文件)。共享库不仅会占据空间,使程序无法在其中存储数据,它们还会使地址空间碎片化,减少可作为连续内存块分配的内存。这对于在拥有 3GB 用户空间的
Windows x86 上运行的程序尤为明显。DLL 在构建时设置了首选的加载地址:当加载 DLL 时,它被映射到处于特定位置的地址空间,除非该位置已经被占用,在这种情况下,它会加载到别处。Windows NT 最初设计时设置了 2GB 可用用户空间,这对于要构建来加载接近 2GB 区域的系统库很有用 —— 使大部分用户区域都可供应用程序自由使用。当用户区域扩展到 3GB 时,系统共享库仍然加载接近 2GB 数据(约为用户空间的一半)。尽管总体用户空间为 3GB,但是不可能分配 3GB 大的内存块,因为共享库无法加载这么大的内存。
在 Windows 中使用
开关,可以将内核空间减少一半,也就是最初设计的大小。在一些情形下,可能耗尽 1GB 内核空间,使 I/O 变得缓慢,且无法正常创建新的用户会话。尽管
开关可能对一些应用程序非常有用,但任何使用它的环境在部署之前都应该进行彻底的负载测试。参见
,获取关于
开关及其优缺点的更多信息的链接。
本机内存泄漏或过度使用本机内存将导致不同的问题,具体取决于您是耗尽了地址空间还是用完了物理内存。耗尽地址空间通常只会发生在 32 位进程上,因为最大 4GB 的内存很容易分配完。64 位进程具有数百或数千 GB 的用户空间,即使您特意消耗空间也很难耗尽这么大的空间。如果您确实耗尽了 Java 进程的地址空间,那么 Java 运行时可能会出现一些陌生现象,本文稍后将详细讨论。当在进程地址空间比物理内存大的系统上运行时,内存泄漏或过度使用本机内存会迫使操作系统交换后备存储器来用作本机进程的虚拟地址空间。访问经过交换的内存地址比读取驻留(在物理内存中)的地址慢得多,因为操作系统必须从硬盘驱动器拉取数据。可能会分配大量内存来用完所有物理内存和所有交换内存(页面空间),在 Linux 上,这将触发内核内存不足(OOM)结束程序,强制结束最消耗内存的进程。在 Windows 上,与地址空间被占满时一样,内存分配将会失败。
同时,如果尝试使用比物理内存大的虚拟内存,显然在进程由于消耗内存太大而被结束之前就会遇到问题。系统将变得异常缓慢,因为它会将大部分时间用于在内存与交换空间之间来回复制数据。当发生这种情况时,计算机和独立应用程序的性能将变得非常糟糕,从而使用户意识到出现了问题。当 JVM 的 Java 堆被交换出来时,垃圾收集器的性能会变得非常差,应用程序可能被挂起。如果一台机器上同时使用了多个 Java 运行时,那么物理内存必须足够分配给所有 Java 堆。
Java 运行时是一个操作系统进程,它会受到我在上一节中列出的硬件和操作系统局限性的限制。运行时环境提供的功能受一些未知的用户代码驱动,这使得无法预测在每种情形中运行时环境将需要何种资源。Java 应用程序在托管 Java 环境中执行的每个操作都会潜在地影响提供该环境的运行时的需求。本节描述 Java 应用程序为什么和如何使用本机内存。
Java 堆是分配了对象的内存区域。大多数 Java SE 实现都拥有一个逻辑堆,但是一些专家级 Java 运行时拥有多个堆,比如实现 Java 实时规范(Real Time Specification for Java,RTSJ)的运行时。一个物理堆可被划分为多个逻辑扇区,具体取决于用于管理堆内存的垃圾收集(GC)算法。这些扇区通常实现为连续的本机内存块,这些内存块受 Java 内存管理器(包含垃圾收集器)控制。
堆的大小可以在 Java 命令行使用
选项来控制(
表示堆的最大大小,
表示初始大小)。尽管逻辑堆(经常被使用的内存区域)可以根据堆上的对象数量和在 GC 上花费的时间而增大和缩小,但使用的本机内存大小保持不变,而且由
值(最大堆大小)指定。大部分 GC 算法依赖于被分配为连续的内存块的堆,因此不能在堆需要扩大时分配更多本机内存。所有堆内存必须预先保留。
保留本机内存与分配本机内存不同。当本机内存被保留时,无法使用物理内存或其他存储器作为备用内存。尽管保留地址空间块不会耗尽物理资源,但会阻止内存被用于其他用途。由保留从未使用的内存导致的泄漏与泄漏分配的内存一样严重。
当使用的堆区域缩小时,一些垃圾收集器会回收堆的一部分(释放堆的后备存储空间),从而减少使用的物理内存。
对于维护 Java 堆的内存管理系统,需要更多本机内存来维护它的状态。当进行垃圾收集时,必须分配数据结构来跟踪空闲存储空间和记录进度。这些数据结构的确切大小和性质因实现的不同而不同,但许多数据结构都与堆大小成正比。
JIT 编译器在运行时编译 Java 字节码来优化本机可执行代码。这极大地提高了 Java 运行时的速度,并且支持 Java 应用程序以与本机代码相当的速度运行。
字节码编译使用本机内存(使用方式与
等静态编译器使用内存来运行一样),但 JIT 编译器的输入(字节码)和输出(可执行代码)必须也存储在本机内存中。包含多个经过 JIT 编译的方法的 Java 应用程序会使用比小型应用程序更多的本机内存。
Java 应用程序由一些类组成,这些类定义对象结构和方法逻辑。Java 应用程序也使用 Java 运行时类库(比如
java.lang.String
)中的类,也可以使用第三方库。这些类需要存储在内存中以备使用。
存储类的方式取决于具体实现。Sun JDK 使用永久生成(permanent generation,PermGen)堆区域。Java 5 的 IBM 实现会为每个类加载器分配本机内存块,并将类数据存储在其中。现代 Java 运行时拥有类共享等技术,这些技术可能需要将共享内存区域映射到地址空间。要理解这些分配机制如何影响您 Java 运行时的本机内存占用,您需要查阅该实现的技术文档。然而,一些普遍的事实会影响所有实现。
从最基本的层面来看,使用更多的类将需要使用更多内存。(这可能意味着您的本机内存使用量会增加,或者您必须明确地重新设置 PermGen 或共享类缓存等区域的大小,以装入所有类)。记住,不仅您的应用程序需要加载到内存中,框架、应用服务器、第三方库以及包含类的 Java 运行时也会按需加载并占用空间。
Java 运行时可以卸载类来回收空间,但是只有在非常严酷的条件下才会这样做。不能卸载单个类,而是卸载类加载器,随其加载的所有类都会被卸载。只有在以下情况下才能卸载类加载器:
Java 堆不包含对表示该类加载器的
java.lang.Cla Loader
对象的引用。
Java 堆不包含对表示类加载器加载的类的任何
java.lang.Cla 对象的引用。
在 Java 堆上,该类加载器加载的任何类的所有对象都不再存活(被引用)。
需要注意的是,Java 运行时为所有 Java 应用程序创建的 3 个默认类加载器(
bootstrap
exte ion
a lication
)都不可能满足这些条件,因此,任何系统类(比如
java.lang.String
)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放。
即使类加载器适合进行收集,运行时也只会将收集类加载器作为 GC 周期的一部分。一些实现只会在某些 GC 周期中卸载类加载器。
也可能在运行时生成类,而不用释放它。许多 JEE 应用程序使用 JavaServer Pages (JSP) 技术来生成 Web 页面。使用 JSP 会为执行的每个 .j 页面生成一个类,并且这些类会在加载它们的类加载器的整个生存期中一直存在 —— 这个生存期通常是 Web 应用程序的生存期。
另一种生成类的常见方法是使用 Java 反射。反射的工作方式因 Java 实现的不同而不同,但 Sun 和 IBM 实现都使用了这种方法,我马上就会讲到。
当使用
java.lang.reflect
API 时,Java 运行时必须将一个反射对象(比如
java.lang.reflect.Field
)的方法连接到被反射到的对象或类。这可以通过使用 Java 本机接口(Java Native Interface,JNI)访问器来完成,这种方法需要的设置很少,但是速度缓慢。也可以在运行时为您想要反射到的每种对象类型动态构建一个类。后一种方法在设置上更慢,但运行速度更快,非常适合于经常反射到一个特定类的应用程序。
Java 运行时在最初几次反射到一个类时使用 JNI 方法,但当使用了若干次 JNI 方法之后,访问器会膨胀为字节码访问器,这涉及到构建类并通过新的类加载器进行加载。执行多次反射可能导致创建了许多访问器类和类加载器。保持对反射对象的引用会导致这些类一直存活,并继续占用空间。因为创建字节码访问器非常缓慢,所以 Java 运行时可以缓存这些访问器以备以后使用。一些应用程序和框架还会缓存反射对象,这进一步增加了它们的本机内存占用。
JNI 支持本机代码(使用 C 和 C++ 等本机编译语言编写的应用程序)调用 Java 方法,反之亦然。Java 运行时本身极大地依赖于 JNI 代码来实现类库功能,比如文件和网络 I/O。JNI 应用程序可能通过 3 种方式增加 Java 运行时的本机内存占用:
JNI 应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中的可执行文件。大型本机应用程序可能仅仅加载就会占用大量进程地址空间。
本机代码必须与 Java 运行时共享地址空间。任何本机代码分配或本机代码执行的内存映射都会耗用 Java 运行时的内存。
某些 JNI 函数可能在它们的常规操作中使用本机内存。
ArrayElements
ArrayRegion
函数可以将 Java 堆数据复制到本机内存缓冲区中,以供本机代码使用。是否复制数据依赖于运行时实现。(IBM Developer Kit for Java 5.0 和更高版本会进行本机复制)。通过这种方式访问大量 Java 堆数据可能会使用大量本机堆。
Java 1.4 中添加的新 I/O (NIO) 类引入了一种基于通道和缓冲区来执行 I/O 的新方式。就像 Java 堆上的内存支持 I/O 缓冲区一样,NIO 添加了对
ByteBuffer
的支持(使用
java.nio.ByteBuffer.allocateDirect()
方法进行分配),
ByteBuffer
受本机内存而不是 Java 堆支持。直接
ByteBuffer
可以直接传递到本机操作系统库函数,以执行 I/O — 这使这些函数在一些场景中要快得多,因为它们可以避免在 Java 堆与本机堆之间复制数据。
对于在何处存储直接
ByteBuffer
数据,很容易产生混淆。应用程序仍然在 Java 堆上使用一个对象来编排 I/O 操作,但持有该数据的缓冲区将保存在本机内存中,Java 堆对象仅包含对本机堆缓冲区的引用。非直接
ByteBuffer
将其数据保存在 Java 堆上的
数组中。图 4 展示了直接与非直接
ByteBuffer
对象之间的区别:
ByteBuffer
对象会自动清理本机缓冲区,但这个过程只能作为 Java 堆 GC 的一部分来执行,因此它们不会自动响应施加在本机堆上的压力。GC 仅在 Java 堆被填满,以至于无法为堆分配请求提供服务时发生,或者在 Java 应用程序中显式请求它发生(不建议采用这种方式,因为这可能导致性能问题)。
发生垃圾收集的情形可能是,本机堆被填满,并且一个或多个直接
ByteBuffers
适合于垃圾收集(并且可以被释放来腾出本机堆的空间),但 Java 堆几乎总是空的,所以不会发生垃圾收集。
应用程序中的每个线程都需要内存来存储器
(用于在调用函数时持有局部变量并维护状态的内存区域)。每个 Java 线程都需要堆栈空间来运行。根据实现的不同,Java 线程可以分为本机线程和 Java 堆栈。除了堆栈空间,每个线程还需要为线程本地存储(thread-local storage)和内部数据结构提供一些本机内存。
堆栈大小因 Java 实现和架构的不同而不同。一些实现支持为 Java 线程指定堆栈大小,其范围通常在 256KB 到 756KB 之间。
尽管每个线程使用的内存量非常小,但对于拥有数百个线程的应用程序来说,线程堆栈的总内存使用量可能非常大。如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更高的内存占用。
Java 运行时善于以不同的方式来处理 Java 堆的耗尽与本机堆的耗尽,但这两种情形具有类似的症状。当 Java 堆耗尽时,Java 应用程序很难正常运行,因为 Java 应用程序必须通过分配对象来完成工作。只要 Java 堆被填满,就会出现糟糕的 GC 性能并抛出表示 Java 堆被填满的
OutOfMemoryError
相反,一旦 Java 运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗尽之后继续正常运行。不一定会发生奇怪的行为,因为需要分配本机内存的操作比需要分配 Java 堆的操作少得多。尽管需要本机内存的操作因 JVM 实现不同而异,但也有一些操作很常见:启动线程、加载类以及执行某种类型的网络和文件 I/O。
本机内存不足行为与 Java 堆内存不足行为也不太一样,因为无法对本机堆分配进行单点控制。尽管所有 Java 堆分配都在 Java 内存管理系统控制之下,但任何本机代码(无论其位于 JVM、Java 类库还是应用程序代码中)都可能执行本机内存分配,而且会失败。尝试进行分配的代码然后会处理这种情况,无论设计人员的意图是什么:它可能通过 JNI 接口抛出一个
OutOfMemoryError
,在屏幕上输出一条消息,发生无提示失败并在稍后再试一次,或者执行其他操作。
缺乏可预测行为意味着无法确定本机内存是否耗尽。相反,您需要使用来自操作系统和 Java 运行时的数据执行诊断。
为了帮助您了解本机内存耗尽如何影响您正使用的 Java 实现,本文的示例代码(参见
)中包含了一些 Java 程序,用于以不同方式触发本机堆耗尽。这些示例使用通过 C 语言编写的本机库来消耗所有本机地址空间,然后尝试执行一些使用本机内存的操作。提供的示例已经过编译,编译它们的指令包含在示例包的顶级目录下的 README.html 文件中。
com.ibm.jtc.demos.NativeMemoryGlutton
类提供了
go leMemory()
方法,它在一个循环中调用
,直到几乎所有本机内存都已耗尽。完成任务之后,它通过以下方式输出分配给标准错误的字节数:
Allocated 1953546736 bytes of native memory before ru ing out
针对在 32 位 Windows 上运行的 Sun 和 IBM Java 运行时的每次演示,其输出都已被捕获。提供的二进制文件已在以下操作系统上进行了测试:
Linux x86
Linux PPC 32
Linux 390 31
Windows x86
使用以下 Sun Java 运行时版本捕获输出:
java version "1.5.0_11"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_11-b03)
Java HotSpot(TM) Client VM (build 1.5.0_11-b03, mixed mode)
使用的 IBM Java 运行时版本为:
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pwi32devifx-20071025 (SR
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Windows XP x86-32 j9vmwi3223-2007100
7 (JIT enabled)
J9VM - 20071004_14218_lHdSMR
- 20070820_1846ifx1_r8
- 200708_10)
- 20071025
com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation
类尝试在耗尽进程地址空间时启动一个线程。这是发现 Java 进程已耗尽内存的一种常用方式,因为许多应用程序都会在其整个生存期启动线程。
当在 IBM Java 运行时上运行时,
StartingAThreadUnderNativeStarvation
演示的输出如下:
Allocated 1019394912 bytes of native memory before ru ing out
JVMDUMP006I Proce ing Dump Event "systhrow", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080323.182114.5172.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080323.182114.5172.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080323.182114.5172.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080323.182114.5172.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080323.182114.5172.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080323.182114.5172.txt
JVMDUMP013I Proce ed 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.A tractCla Loader.fillCache(A tractCla Loader.java:69)
at com.ibm.oti.vm.A tractCla Loader.getResourceAsStream(A tractCla Loader.java:113)
at java.util.ResourceBundle$1.run(ResourceBundle.java:1101)
at java.security.Acce Controller.doPrivileged(Acce Controller.java:197)
at java.util.ResourceBundle.loadBundle(ResourceBundle.java:1097)
at java.util.ResourceBundle.findBundle(ResourceBundle.java:942)
at java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:779)
at java.util.ResourceBundle.getBundle(ResourceBundle.java:716)
at com.ibm.oti.vm.MsgHelp.setLocale(MsgHelp.java:103)
at com.ibm.oti.util.Msg$1.run(Msg.java:44)
at java.security.Acce Controller.doPrivileged(Acce Controller.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 fork OS thread
at java.lang.Thread.startImpl(Native Method)
at java.lang.Thread.start(Thread.java:979)
at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)
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 运行时上执行相同的测试案例时,会生成以下控制台输出:
Allocated 1953546736 bytes of native memory before ru ing out
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:574)
at com.ibm.jtc.demos.StartingAThreadUnderNativeStarvation.main(
StartingAThreadUnderNativeStarvation.java:22)
尽管堆栈轨迹和错误消息稍有不同,但其行为在本质上是一样的:本机分配失败并抛出
java.lang.OutOfMemoryError
。此场景中抛出的
OutOfMemoryError
与由于 Java 堆耗尽而抛出的错误的惟一区别在于消息。
com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation
类尝试在地址空间耗尽时分配一个直接(也就是受本机支持的)
java.nio.ByteBuffer
对象。当在 IBM Java 运行时上运行时,它生成以下输出:
Allocated 1019481472 bytes of native memory before ru ing out
JVMDUMP006I Proce ing Dump Event "uncaught", detail
"java/lang/OutOfMemoryError" - Please Wait.
JVMDUMP007I JVM Requesting Snap Dump using 'C:\Snap0001.20080324.100721.4232.trc'
JVMDUMP010I Snap Dump written to C:\Snap0001.20080324.100721.4232.trc
JVMDUMP007I JVM Requesting Heap Dump using 'C:\heapdump.20080324.100721.4232.phd'
JVMDUMP010I Heap Dump written to C:\heapdump.20080324.100721.4232.phd
JVMDUMP007I JVM Requesting Java Dump using 'C:\javacore.20080324.100721.4232.txt'
JVMDUMP010I Java Dump written to C:\javacore.20080324.100721.4232.txt
JVMDUMP013I Proce ed Dump Event "uncaught", detail "java/lang/OutOfMemoryError".
Exception in thread "main" java.lang.OutOfMemoryError:
Unable to allocate 1048576 bytes of direct memory after 5 retries
at java.nio.DirectByteBuffer.init(DirectByteBuffer.java:167)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303)
at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)
Caused by: java.lang.OutOfMemoryError
at sun.misc.U afe.allocateMemory(Native Method)
at java.nio.DirectByteBuffer.init(DirectByteBuffer.java:154)
... 2 more
在此场景中,抛出了
OutOfMemoryError
,它会触发默认的错误文档。
OutOfMemoryError
到达主线程堆栈的顶部,并在
上输出。
当在 Sun Java 运行时上运行时,此测试案例生成以下控制台输出:
Allocated 1953546760 bytes of native memory before ru ing out
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.U afe.allocateMemory(Native Method)
at java.nio.DirectByteBuffer.init(DirectByteBuffer.java:99)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
at com.ibm.jtc.demos.DirectByteBufferUnderNativeStarvation.main(
DirectByteBufferUnderNativeStarvation.java:29)
查阅供应商文档
本文提供的指南是一般的调试原则,可用于理解本机内存耗尽场景。您的运行时供应商可能提供了自己的调试说明,供应商期望您按照这些说明与其支持团队联系。如果您要与运行时供应商(包括 IBM)合作解决问题,请始终检查其调试和诊断文档,查看在提交问题报告时应该执行哪些步骤。
当出现
java.lang.OutOfMemoryError
或看到有关内存不足的错误消息时,要做的第一件事是确定哪种类型的内存被耗尽。最简单的方式是首先检查 Java 堆是否被填满。如果 Java 堆未导致
OutOfMemory
条件,那么您应该分析本机堆使用情况。
检查堆使用情况的方法因 Java 实现不同而异。在 Java 5 和 6 的 IBM 实现上,当抛出
OutOfMemoryError
时会生成一个 javacore 文件来告诉您。javacore 文件通常在 Java 进程的工作目录中生成,以 javacore.
.txt 的形式命名。如果您在文本编辑器中打开该文件,可以看到以下信息:
0SECTION
MEMINFO subcomponent dump routine
=================================
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 cla 00147F80
J9AllocateObject() returning NULL!
意味着 Java 堆分配例程未成功完成,并且将抛出
OutOfMemoryError
也可能由于垃圾收集器运行太频繁(意味着堆被填满了并且 Java 应用程序的运行速度将很慢或停止运行)而抛出
OutOfMemoryError
。在这种情况下,您可能想要 Heap Space Free 值非常小,GC 轨迹将显示以下消息之一:
1STGCHTYPE
GC History
3STHSTTYPE
09:59:01:632262775 GMT j9mm.83 -
Forcing J9AllocateObject()
to fail due to exce ive GC
1STGCHTYPE
GC History
3STHSTTYPE
09:59:01:632262775 GMT j9mm.84 -
Forcing
J9AllocateIndexableObject() to fail due to exce ive GC
当 Sun 实现耗尽 Java 堆内存时,它使用异常消息来显示它耗尽的是 Java 堆:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap ace
IBM 和 Sun 实现都拥有一个详细的 GC 选项,用于在每个 GC 周期生成显示堆填充情况的跟踪数据。此信息可使用工具(比如 IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer (GCMV))来分析,以显示 Java 堆是否在增长(参见
如果您确定内存耗尽情况不是由 Java 堆耗尽引起的,那么下一步就是分析您的本机内存使用情况。
Windows 提供的 PerfMon 工具可用于监控和记录许多操作系统和进程指标,包括本机内存使用(参见
)。它允许实时跟踪
计数器
,或将其存储在日志文件中以供离线查看。使用 Private Bytes 计数器显示总体地址空间使用情况。如果显示值接近于用户空间的限制(前面已经讨论过,介于 2 到 3GB 之间),您应该会看到本机内存耗尽情况。
Linux 没有类似于 PerfMon 的工具,但是它提供了几个替代工具。命令行工具(比如 、
)能够显示应用程序的本机内存占用情况。尽管获取进程内存使用情况的实时快照非常有用,但通过记录内存随时间的使用情况,您能够更好地理解本机内存是如何被使用的。为此,能够采取的一种方式是使用 GCMV。
GCMV 最初编写用于分析冗长的 GC 日志,允许用户在调优垃圾收集器时查看 Java 堆使用情况和 GC 性能的变化。GCMV 后来进行了扩展,支持分析其他数据源,包括 Linux 和 AIX 本机内存数据。GCMV 是作为 IBM Su ort A istant (ISA) 的插件发布的。
要使用 GCMV 分析 Linux 本机内存配置文件,您首先必须使用脚本收集本机内存数据。GCMV 的 Linux 本机内存分析器通过根据时间戳隔行扫描的方式,读取 Linux 命令的输出。GCMV 提供了一个脚本来帮助以正确形式记录收集数据。要找到该脚本:
下载并***
Version 4(或更高版本),然后*** GCMV 工具插件。
启动 ISA。
从菜单栏单击
Help Help Contents
,打开 ISA 帮助菜单。
在左侧窗格的
Tool:IBM
Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer
Using the Garbage Collection and Memory Visualizer Su orted Data
Types Native memory Linux native memory
下找到 Linux 本机内存说明。
图 5 显示了该脚本在 ISA 帮助文件中的位置。如果您的帮助文件中没有 GCMV Tool 条目,很可能是因为您没有*** GCMV 插件。
GCMV 帮助文件中提供的脚本使用的 命令仅适用于最新的 版本。在一些旧的 Linux 分发版中,帮助文件中的命令将会生成错误信息。要查看您的 Linux 分发版上的行为,可以尝试运行 -o pid,vsz=VSZ,r =RSS
。如果您的 版本支持新的命令行参数语法,那么得到的输出将类似于:
如果您的 版本不支持新语法,得到的输出将类似于:
PID VSZ,r =RSS
如果您在一个较老的 版本上运行,可以修改本机内存脚本,将 -p $PID -o pid,vsz=VSZ,r =RSS
行替换为 -p $PID -o pid,vsz,r 将帮助面板中的脚本复制到一个文件中(在本例中名为 memscript.sh),找到您想要监控的 Java 进程的进程 ID (PID)(本例中为 1234)并运行:
./memscript.sh 1234 .out
这会把本机内存日志写入到 .out 中。要分析内存使用情况:
在 ISA 中,从
Launch Activity
下拉菜单选择
Analyze Problem
选择接近
Analyze Problem
面板顶部的
标签。
IBM Monitoring and Diagnostic Tools for Java - Garbage Collection and Memory Visualizer
单击接近工具面板底部的
按钮。
单击 Browse 按钮并找到日志文件。单击
启动 GCMV。
一旦您拥有了本机内存随时间的使用情况的配置文件,您需要确定是存在本机内存泄漏,还是在尝试在可用空间中做太多事情。即使对于运行良好的 Java 应用程序,其本机内存占用也不是从启动开始就一成不变的。一些 Java 运行时系统(尤其是 JIT 编译器和类加载器)会不断初始化,这会消耗本机内存。初始化增加的内存将高居不下,但是如果初始本机内存占用接近于地址空间的限制,那么仅这个前期阶段就足以导致本机内存耗尽。图 6 给出了一个 Java 压力测试示例中的 GCMV 本机内存使用情况,其中突出显示了前期阶段。
本机内存占用也可能应工作负载不同而异。如果您的应用程序创建了较多进程来处理传入的工作负载,或者根据应用于系统的负载量按比例分配本机存储(比如直接
ByteBuffer
),则可能由于负载过高而耗尽本机内存。
由于 JVM 前期阶段的本机内存增长而耗尽本机内存,以及内存使用随负载增加而增加,这些都是尝试在可用空间中做太多事情的例子。在这些场景中,您的选择是:
减少本机内存使用。
缩小 Java 堆大小是一个好的开端。
限制本机内存使用。
如果您的本机内存随负载增加而增加,可以采取某种方式限制负载或为负载分配的资源。
增加可用地址空间。
这可以通过以下方式实现:调优您的操作系统(例如,在 Windows 上使用
开关增加用户空间,或者在 Linux 上使用庞大的内核空间),更换平台(Linux 通常拥有比 Windows 更多的用户空间),或者
一种实际的本机内存泄漏表现为本机堆的持续增长,这些内存不会在移除负载或运行垃圾收集器时减少。内存泄漏程度因负载不同而不同,但泄漏的总内存不会下降。泄漏的内存不可能被引用,因此它可以被交换出去,并保持被交换出去的状态。
当遇到内存泄漏时,您的选择很有限。您可以增加用户空间(这样就会有更多的空间供泄漏),但这仅能延缓最终的内存耗尽。如果您拥有足够的物理内存和地址空间,并且会在进程地址空间耗尽之前重启应用程序,那么可以允许地址空间继续泄漏。
一旦确定本机内存被耗尽,下一个逻辑问题是:是什么在使用这些内存?这个问题很难回答,因为在默认情况下,Windows 和 Linux 不会存储关于分配给特定内存块的代码路径的信息。
当尝试理解本机内存都到哪里去了时,您的第一步是粗略估算一下,根据您的 Java 设置,将会使用多少本机内存。如果没有对 JVM 工作机制的深入知识,很难得出精确的值,但您可以根据以下指南粗略估算一下:
Java 堆占用的内存至少为
每个 Java 线程需要堆栈空间。堆栈空间因实现不同而异,但是如果使用默认设置,每个线程至多会占用 756KB 本机内存。
ByteBuffer
至少会占用提供给
allocate()
例程的内存值。
如果总数比您的最大用户空间少得多,那么您很可能不安全。Java 运行时中的许多其他组件可能会分配大量内存,进而引起问题。但是,如果您的初步估算值与最大用户空间很接近,则可能存在本机内存问题。如果您怀疑存在本机内存泄漏,或者想要准确了解内存都到哪里去了,使用一些工具将有所帮助。
Microsoft 提供了 UMDH(用户模式转储堆)和 LeakDiag 工具来在 Windows 上调试本机内存增长(参见
)。这两个工具的机制相同:记录特定内存区域被分配给了哪个代码路径,并提供一种方式来定位所分配的内存不会在以后被释放的代码部分。我建议您查阅文章 “Umdhtools.exe:如何使用 Umdh.exe 发现 Windows 上的内存泄漏”,获取 UMDH 的使用说明(参见
)。在本文中,我将主要讨论 UMDH 在分析存在泄漏的 JNI 应用程序时的输出。
本文的
包含一个名为
LeakyJNIA 的 Java 应用程序,它循环调用一个 JNI 方法来泄漏本机内存。UMDH 命令获取当前的本机堆的快照,以及分配每个内存区域的代码路径的本机堆栈轨迹快照。通过获取两个快照,并使用 UMDH 来分析差异,您会得到两个快照之间的堆增长报告。
LeakyJNIA ,差异文件包含以下信息:
// _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 allocatio //
... 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 allocatio between before and after log
NEW_COUNT - number of allocatio in after log
OLD_COUNT - number of allocatio in before log
TRACEID - decimal index of the stack trace in the trace database
(can be used to search for allocation i tances 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
,可以找到如下信息:
000000AD bytes in 0x1 allocatio (@ 0x00000031 + 0x0000001F) by: BackTrace00468
ntdll!RtlpNtMakeTemporaryKey+000074D0
ntdll!RtlInitializeSListHead+00010D08
ntdll!wc cat+00000224
leakyjnia !Java_com_ibm_jtc_demos_LeakyJNIA _nativeMethod+000000D6
这显示内存泄漏来自
Java_com_ibm_jtc_demos_LeakyJNIA _nativeMethod
函数中的 leakyjnia .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 相同。
LeakyJNIA 非常简单,能够在 Valgrind 下运行。当模拟的程序结束时,Valgrind
memcheck
工具能够输出泄漏的内存的汇总信息。默认情况下,
LeakyJNIA 程序会一直运行,要使其在固定时期之后关闭,可以将运行时间(以秒为单位)作为惟一的命令行参数进行传递。
一些 Java 运行时以非常规的方式使用线程堆栈和处理器寄存器,这可能使一些调试工具产生混淆,这些工具要求本机程序遵从寄存器使用和堆栈结构的标准约定。当使用 Valgrind 调试存在内存泄漏的 JNI 应用程序时,您可以发现许多与内存使用相关的警告,并且一些线程堆栈看起来很奇怪,这是由 Java 运行时在内部构造其数据的方式所导致的,不用担心。
要使用 Valgrind
memcheck
工具跟踪
LeakyJNIA ,(在一行上)使用以下命令:
valgrind --trace-children=yes --leak-check=full
java -Djava.library.path=. com.ibm.jtc.demos.LeakyJNIA 10
--trace-children=yes
选项使 Valgrind 跟踪由 Java 启动器启动的任何进程。一些 Java 启动器版本会重新执行其本身(它们从头重新启动其本身,再次设置环境变量来改变行为)。如果您未指定
--trace-children
,您将不能跟踪实际的 Java 运行时。
--leak-check=full
选项请求在代码运行结束时输出对泄漏的代码区域的完整堆栈轨迹,而不只是汇总内存的状态。
当该命令运行时,Valgrind 输出许多警告和错误(在此环境中,其中大部分都是无意义的),最后按泄漏的内存量升序输出存在泄漏的调用堆栈。在 Linux x86 上,针对
LeakyJNIA 的 Valgrind 输出的汇总部分结尾如下:
==20494== 8,192 bytes in 8 blocks are po ibly lost in lo record 36 of 45
==20494==
at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==
by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIA _nativeMethod
(in /home/andhall/LeakyJNIA /libleakyjnia .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 lo record 42 of 45
==20494==
at 0x4024AB8: malloc (vg_replace_malloc.c:207)
==20494==
by 0x460E49D: Java_com_ibm_jtc_demos_LeakyJNIA _nativeMethod
(in /home/andhall/LeakyJNIA /libleakyjnia .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==
po ibly lost: 8,600 bytes in 11 blocks.
==20494==
still reachable: 5,156,340 bytes in 980 blocks.
==20494==
su re ed: 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.LeakyJNIA .nativeMethod()
方法泄漏的。
也可以使用一些专用调试应用程序来调试本机内存泄漏。随着时间的推移,会有更多工具(包括开源和专用的)被开发出来,这对于研究当前技术的发展现状很有帮助。
就目前而言,使用免费工具调试 Linux 上的本机内存泄漏比在 Windows 上完成相同的事情更具挑战性。UMDH 支持
调试 Windows 上本机内存泄漏,在 Linux 上,您可能需要进行一些传统的调试,而不是依赖工具来解决问题。下面是一些建议的调试步骤:
提取测试案例。
生成一个独立环境,您需要能够在该环境中再现本机内存泄漏。这将使调试更加简单。
尽可能缩小测试案例。
尝试禁用函数来确定是哪些代码路径导致了本机内存泄漏。如果您拥有自己的 JNI 库,可以尝试一次禁用一个来确定是哪个库导致了内存泄漏。
缩小 Java 堆大小。
Java 堆可能是进程的虚拟地址空间的最大使用者。通过减小 Java 堆,可以将更多空间提供给本机内存的其他使用者。
关联本机进程大小。
一旦您获得了本机内存随时间的使用情况,可以将其与应用程序工作负载和 GC 数据比较。如果泄漏程度与负载级别成正比,则意味着泄漏是由每个事务或操作路径上的某个实体引起的。如果当进行垃圾收集时,本机进程大小显著减小,这意味着您没遇到内存泄漏,您拥有的是具有本机支持的对象组合(比如直接
ByteBuffer
)。通过缩小 Java 堆大小(从而迫使垃圾收集更频繁地发生),或者在一个对象缓存中管理对象(而不是依赖于垃圾收集器来清理对象),您可以减少本机支持对象持有的内存量。
如果您确定内存泄漏或增长来自于 Java 运行时本身,您可能需要联系运行时供应商来进一步调试。
使用 32 位 Java 运行时很容易遇到本机内存耗尽的情况,因为地址空间相对较小。32 位操作系统提供的 2 到 4GB 用户空间通常小于系统附带的物理内存量,而且现代的数据密集型应用程序很容易耗尽可用空间。
如果 32 位地址空间不够您的应用程序使用,您可以通过移动到 64 位 Java 运行时来获得更多用户空间。如果您运行的是 64 位操作系统,那么 64 位 Java 运行时将能够满足海量 Java 堆的需求,还会减少与地址空间相关的问题。表 2 列出了 64 位操作系统上目前可用的用户空间。
操作系统
默认用户空间大小
Windows x86-64
Windows Itanium
Linux x86-64
Linux PPC64
Linux 390 64
然而,移动到 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 位运行时上的 Java 对象会占用比 32 位运行时上包含相同数据的对象更多的空间。更大的对象意味着要使用更大的堆来持有相同的数据量,同时保持类似的 GC 性能,这使操作系统和硬件缓存效率更低。令人惊讶的是,更大的 Java 堆并不一定意味着更长的 GC 暂停时间,因为堆上的活动数据量可能不会增加,并且一些 GC 算法在使用更大的堆时效率更高。
一些现代 Java 运行时包含减轻 64 位 “对象膨胀” 和改善性能的技术。这些功能在 64 位运行时上使用更短的引用。这在 IBM 实现中称为
压缩引用
,而在 Sun 实现中称为
压缩 oop
对 Java 运行时性能的比较研究不属于本文讨论范围,但是如果您正在考虑移动到 64 位,尽早测试应用程序以理解其执行原理会很有帮助。由于更改地址大小会影响到 Java 堆,所以您将需要在新架构上重新调优您的 GC 设置,而不是仅仅移植现有设置。
在设计和运行大型 Java 应用程序时,理解本机内存至关重要,但是这一点通常被忽略,因为它与复杂的硬件和操作系统细节密切相关,Java 运行时的目的正是帮助我们规避这些细节。JRE 是一个本机进程,它必须在由这些纷繁复杂的细节定义的环境中工作。要从 Java 应用程序中获得最佳的性能,您必须理解应用程序如何影响 Java 运行时的本机内存使用。
耗尽本机内存与耗尽 Java 堆很相似,但它需要不同的工具集来调试和解决。修复本机内存问题的关键在于理解运行您的 Java 应用程序的硬件和操作系统施加的限制,并将其与操作系统工具知识结合起来,监控本机内存使用。通过这种方法,您将能够解决 Java 应用程序产生的一些非常棘手的问题。
下载方法
本机内存示例代码
j-nativememory-linux.zip
”(Holly Cummi ,developerWorks,2007 年 10 月):了解如何下载和*** GCMV,以及使用它分析冗长的垃圾收集数据。
”(Emma Shepherd 等,developerWorks,2004 年 11 月):了解如何使用 PerfMon 及其他工具监控 Java 应用程序的 Windows 内存使用情况。
”(Dmitry Jemerov 的网络博客,2005 年 2 月):LinkDiag 拦截进程中的内存分配函数,记录每次分配的调用栈,并根据调用栈记录分配日志。
”(Microsoft Help and Su ort,2007 年 4 月):UMDH 是另一个 Microsoft 工具,其工作原理与 LeakDiag 类似。
”(Phil Vickers 和 Amar Devegowda,IBM Java Technology Centre,2005 年 12 月):了解如何最大化 32 位 Windows 系统上的 IBM Java 内存地址空间。
”(Dave Draeger 等,developerWorks,2007 年 5 月):IBM Guided Activity A istant 提供了帮助您调试常见问题(包括 Java 内存耗尽)的工作流程。
”(Raymond Chen,The Old New Thing,2004 年 8 月):简单讨论使用
开关调整 Windows 内核空间的一些争议。
(Raymond Chen,The Old New Thing,2004 年 8 月):一些与 Windows
开关相关的文章和博客链接。
:IBM SDK for Java 将指导您解决常见 Java 编程问题。
,获取关于这些和其他技术主题的图书。
:查找数百篇关于 Java 编程各方面的文章。
获得产品和技术
:下载 Valgrind I trumentation Framework,其中包括内存错误检测器。
:下载 Debug Malloc 库。
:下载 ccmalloc 内存调试器库。
:下载 NJAMD(它不仅仅是另一个 Malloc 调试器)内存调试器库。
:访问 IBM Java 工具页面。
:这个免费支持框架包含 Garbage Collection and Memory Visualizer 和 IBM Guided Activity A istant 等工具,可用于调试本机内存耗尽情况。
并加入
Andrew Hall 于 2004 年加入 IBM Java Technology Centre,他在 Java System Test 小组工作了两年。然后在 Java 服务团队工作了 18 个月,其间,他在多个平台上调试了数十个本机内存问题。他目前是 Java Reliability, Availability and Serviceability 团队的成员。在业余生活中,他喜欢阅读、摄影和玩魔术。
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Linux
ArticleID=388310
ArticleTitle=内存详解
publish-date=05112009
author1-email=andhall_@uk.ibm.com
author1-email-cc=jaloi@us.ibm.com
文本框在 My developerWorks 中查找包含该标签的所有内容。
滑动条
调节标签的数量。
热门标签
显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。
我的标签
显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。
使用搜索文本框在 My developerWorks 中查找包含该标签的所有内容。
热门标签
显示了特定专区最受欢迎的标签(例如 Java technology,Linux,WebSphere)。
我的标签
显示了特定专区您标记的标签(例如 Java technology,Linux,WebSphere)。
搜索所有标签 热门文章标签 | | 我的文章标签
分享此页面:
关注 developerWorks:
查找软件
关于 developerWorks
相关资源