您的位置:首页 > 编程语言 > Java开发

jvm内存溢出分析及解决

2012-04-28 17:23 288 查看
1,JVM specification s(JVM 规范) 对JVM 内存的描述

2,Sun 的JVM 的内存机制。

JVM specification 对JVM 内存的描述

首先我们来了解JVM specification 中的JVM 整体架构。如下图:



主要包括两个子系统和两个组件: Class loader(类装载器) 子系统,

Execution engine(执行引擎) 子系统;Runtime data area (运行时数据区域)

组件, Native interface(本地接口)组件。

Class loader 子系统的作用:根据给定的全限定名类名(如

java.lang.Object)来装载class 文件的内容到Runtime data area 中的method

area(方法区域)。Javsa 程序员可以extends java.lang.ClassLoader 类来写自

己的Class loader。

Execution engine 子系统的作用:执行classes 中的指令。任何JVM

specification 实现(JDK)的核心是Execution engine, 换句话说:Sun 的JDK

和IBM 的JDK 好坏主要取决于他们各自实现的Execution engine 的好坏。每个

运行中的线程都有一个Execution engine 的实例。

Native interface 组件:与native libraries 交互,是其它编程语言交

互的接口。

Runtime data area 组件:这个组件就是JVM 中的内存。下面对这个部分

进行详细介绍。

Runtime data area 的整体架构图



Runtime data area 主要包括五个部分:Heap (堆), Method Area(方法区域),

Java Stack(java 的栈), Program Counter(程序计数器), Native method

stack(本地方法栈)。Heap 和Method Area 是被所有线程的共享使用的;而Java

stack, Program counter 和Native method stack 是以线程为粒度的,每个线

程独自拥有。

Heap

Java 程序在运行时创建的所有类实或数组都放在同一个堆中。而一个Java 虚拟

实例中只存在一个堆空间,因此所有线程都将共享这个堆。每一个java 程序独

占一个JVM 实例,因而每个java 程序都有它自己的堆空间,它们不会彼此干扰。

但是同一java 程序的多个线程都共享着同一个堆空间,就得考虑多线程访问对

象(堆数据)的同步问题。(这里可能出现的异常java.lang.OutOfMemoryError:

Java heap space)

Method area

在Java 虚拟机中,被装载的class 的信息存储在Method area 的内存中。当虚

拟机装载某个类型时,它使用类装载器定位相应的class 文件,然后读入这个

class 文件内容并把它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并

将这些信息存储到方法区。该类型中的类(静态)变量同样也存储在方法区中。

与Heap 一样,method area 是多线程共享的,因此要考虑多线程访问的同步问

题。比如,假设同时两个线程都企图访问一个名为Lava 的类,而这个类还没有

内装载入虚拟机,那么,这时应该只有一个线程去装载它,而另一个线程则只能

等待。(这里可能出现的异常java.lang.OutOfMemoryError: PermGen full)

Java stack

Java stack 以帧为单位保存线程的运行状态。虚拟机只会直接对Java

stack 执行两种操作:以帧为单位的压栈或出栈。每当线程调用一个方法的时候,

就对当前状态作为一个帧保存到java stack 中(压栈);当一个方法调用返回时,

从java stack 弹出一个帧(出栈)。栈的大小是有一定的限制,这个可能出现

StackOverFlow 问题。下面的程序可以说明这个问题。

public class TestStackOverFlow {

public static void main(String[] args) {

Recursive r = new Recursive();

r.doit(10000);

// Exception in thread "main"

java.lang.StackOverflowError

}

}

class Recursive {

public int doit(int t) {

if (t <= 1) {

return 1;

}

return t + doit(t - 1);

}

}

Program counter

每个运行中的Java 程序,每一个线程都有它自己的PC 寄存器,也是该线程启动

时创建的。PC 寄存器的内容总是指向下一条将被执行指令的饿“地址

”,这里的“地址”可以是一个本地指针,也可以是在方法区

中相对应于该方法起始指令的偏移量。

Native method stack

对于一个运行中的Java 程序而言,它还能会用到一些跟本地方法相关的数据区。

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制

的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,不止与

此,它还可以做任何它想做的事情。比如,可以调用寄存器,或在操作系统中分

配内存等。总之,本地方法具有和JVM 相同的能力和权限。(这里出现JVM 无法

控制的内存溢出问题native heap OutOfMemory )

Sun JVM 中对JVM Specification 的实现(内存部分)

JVM Specification 只是抽象的说明了JVM 实例按照子系统、内存区、数据类型

以及指令这几个术语来描述的,但是规范并非是要强制规定Java 虚拟机实现内

部的体系结构,更多的是为了严格地定义这些实现的外部特征。

Sun JVM 实现中:Runtime data area(JVM 内存) 五个部分中的Java Stack ,

Program Counter, Native method stack 三部分和规范中的描述基本一致;但

对Heap 和Method Area 进行了自己独特的实现。这个实现和Sun JVM 的Garbage

collector(垃圾回收)机制有关,下面的章节进行详细描述。

垃圾分代回收算法(Generational Collecting)

基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、

持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。

现在的垃圾回收器(从J2SE1.2 开始)都是使用此算法的。



如上图所示,为Java 堆中的各代分布。

1. Young(年轻代)JVM specification 中的Heap 的一部份

年轻代分三个区。一个Eden 区,两个Survivor 区。大部分对象在Eden 区中生

成。当Eden 区满时,还存活的对象将被复制到Survivor 区(两个中的一个),

当这个Survivor 区满时,此区的存活对象将被复制到另外一个Survivor 区,当

这个Survivor 去也满了的时候,从第一个Survivor 区复制过来的并且此时还存

活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor 的

两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden 复制过来

对象,和从前一个Survivor 复制过来的对象,而复制到年老区的只有从第一个

Survivor 去过来的对象。而且,Survivor 区总有一个是空的。

2. Tenured(年老代)JVM specification 中的Heap 的一部份

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对

象。

3. Perm(持久代) JVM specification 中的Method area

用于存放静态文件,如今Java 类、方法等。持久代对垃圾回收没有显著影响,

但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时

候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大

小通过-XX:MaxPermSize=进行设置

而在出现内存泄露的机器上,其日志显示是无法创建本地线程的原因所引起的。这里的异常信息是:java.lang.OutOfMemoryError: unable to create new native thread,对应上述内存溢出的第4种场景。尽管可以初步怀疑是虚拟机参数的设置导致的问题,但实际上还是需要确认系统在自动化场景下有没有其他内存泄露问题。

重新跑自动化,并中间使用“jstat –gcutil 进程ID 1000 3 >>jstat.txt”命令,每隔3秒查看一下虚拟机堆空间的回收情况。在运行了三个多小时后,发行server.log种已经出现了该OutOfMemory的异常信息。此时查看了jstat.txt文件,发现从自动化开始运行一直到堆栈溢出,内存回收都很正常。全部垃圾回收时间花费了5秒左右,且未有full gc,全为young gc的时间。持久区(Perm)、年老区(Old),分别占用了25%、19%左右的空间。且使用“top”命令监测中间CPU和内存占用都比较稳定,没有激增的现象。

使用“jmap –hito 进程ID”查看内存对象统计,发现没有业务逻辑相关的类导致的泄露问题。系统中创建最多的就是与Sting相关的char数组对象。这个也是正常情况,排除程序级别的内存泄漏问题。也就是说堆栈溢出不是1和2的两种情况。

此时再分析server.log种的日志信息,得知是无法创建本地线程所致的问题。也就是说在压力环境下拥有大量的线程,或者本地内存耗尽时,企图创建新的线程时抛出。而系统能创建的线程数的计算公式如下: 

(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads 

MaxProcessMemory 指的是一个进程的最大内存

JVMMemory JVM内存

ReservedOsMemory 保留的操作系统内存

ThreadStackSize 线程栈的大小

【解决方法】:

针对无法创建更多本地线程的情况,调整线程栈的大小,添加-Xss选项,设置为256k后再跑自动化,发现问题解决。

 JAVA_OPTS="-Xms2048M -Xmx2048M -Xmn512M -Xss256k -XX:PermSize=512M….”
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息