2 Java内存区域与内存溢出异常

2.1 概述

虚拟机提供的最大的能力:自动内存管理。不再需要用户为每一个操作去写配对的delete、free代码,不需要手动管理内存的申请和释放,不容易出现内存泄漏的问题。这也符合并奠定了Java的操作哲学,通过各种各样的运行时接管一些常规的传统语言的操作

2.2 运行时内存区域

内存区域

  1. 程序计数器:当前线程所执行的字节码的行号指示器。多线程通过线程切换、分配处理器执行,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响、独立存储,是线程私有的内存。
  2. Java虚拟机栈:Java方法执行的内存模型。每个方法执行的时候,创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。线程私有的内存。局部变量表存放了编译器可知的各种Java虚拟机基本变量类型、对象引用、返回地址类型,局部变量表所需要的空间是在编译期间分配完成的,运行过程中,当进入一个方法时,这个方法需要再栈帧中分配多大的局部变量空间是完全确定的。
  3. 本地方法栈:本地方法的栈帧。也是线程私有的内存。
  4. 堆:用于存放对象实例。被所有线程共享的一块内存区域,是垃圾收集器管理的内存区域。可以处在物理上不连续的内存空间。通过虚拟机参数-Xmx和Xms控制堆的大小。当Java堆无法再扩展时,就会抛出OOM异常。
  5. 方法区:存储已经被虚拟机加载的类信息、常量、静态变量,即时编译后的代码缓存等数据。
  6. 运行时常量池:方法区的一部分。Class文件包括版本、字段、方法、接口等描述信息,还有常量池表,用来存放编译期产生的各种字面常量与符号引用。运行时常量池具有动态性,不仅包括编译期产生的常量与符号引用,还包括运行期将常量放入常量池中,例如通过String的intern()方法创建的字符串常量。常量池无法再申请到内存时会抛出OOM异常。
  7. 直接内存:NIO的通过和缓冲区可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样操作能够显著提高性能,避免在Java堆和Native堆中来回复制数据。本机的内存分配不会受到Java堆大小的限制。

内存溢出

2.3 虚拟机对象

对象创建的过程

创建过程

  1. 遇到一个new命令
  2. 检查类。检查这个指令的参数,是否能在常量池中定位到一个类的符号引用,如果没有那么必须执行类加载过程,如果检查到这个符号,则代表这个类已经被加载、解析和初始化过。
  3. 分配内存。对象所需要的内存大小在类加载完成后便可以确定。
    1. 指针碰撞:假如是规整内存,使用过的内存和未使用的内存都会规整存放,两者中间通过分界点指示器分割,那么分配内存就是移动一下分界点的指针。空闲内存列表:如果内存并不规整,虚拟机维护了一个可用的内存列表,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新可用内存列表。
    2. Serial和ParNew等收集器带有内存压缩能力,采用了指针碰撞方式。CMS标记清楚算法收集器采用空闲内存列表的方式分配内存。
    3. 并发安全问题:CAS保证内存分配操作的原子性。本地线程分配缓存,每个线程在Java堆中预先分配一小块内存,内存分配动作按照线程划分在不同的空间中运行,
  4. 初始化:初始化内存值,将分配到的内存初始化位0.初始化对象头,包括类指针,对象的哈希码,对象的GC分代年龄等信息。
  5. 执行用户初始化操作:执行方法,按照程序员的意愿对对象进行初始化。

对象的内存布局

对象在堆内存中的存储布局分为三部分:对象头,实例数据,对齐填充。
alt text

  1. 对象头:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。还包括类型指针,指向它的类型元数据的指针,通过这个指针可以确定对象属于哪个类的实力。对于Java数组这里还会保存数组的长度。
  2. 实例数据:真正存储的有效信息,程序代码里定义的各种类型段内容。
  3. 对齐填充。8字节的整数倍。

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。

  1. 句柄访问。对象移动的时候改变句柄值即可。
    alt text
  2. 访问速度快减少一次寻址操作。
    alt text

2.4 OOM

1.什么是OOM?

OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。

2.为什么会OOM?

为什么会没有内存了呢?原因不外乎有两点:

  • 1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
  • 2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。主要分为以下两种情况:
    • 内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
    • 内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。

在之前没有垃圾自动回收的日子里,比如C语言和C++语言,我们必须亲自负责内存的申请与释放操作,如果申请了内存,用完后又忘记了释放,比如C++中的new了但是没有delete,那么就可能造成内存泄露。偶尔的内存泄露可能不会造成问题,而大量的内存泄露可能会导致内存溢出。

而在Java语言中,由于存在了垃圾自动回收机制,所以,我们一般不用去主动释放不用的对象所占的内存,也就是理论上来说,是不会存在“内存泄露”的。但是,如果编码不当,比如,将某个对象的引用放到了全局的Map中,虽然方法结束了,但是由于垃圾回收器会根据对象的引用情况来回收内存,导致该对象不能被及时的回收。如果该种情况出现次数多了,就会导致内存溢出,比如系统中经常使用的缓存机制。Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。

3.OOM类型

JVM内存模型:

按照JVM规范,JAVA虚拟机在运行时会管理以下的内存区域:

  • 程序计数器:当前线程执行的字节码的行号指示器,线程私有
  • JAVA虚拟机栈:Java方法执行的内存模型,每个Java方法的执行对应着一个栈帧的进栈和出栈的操作。
  • 本地方法栈:类似“ JAVA虚拟机栈 ”,但是为native方法的运行提供内存环境。
  • JAVA堆:对象内存分配的地方,内存垃圾回收的主要区域,所有线程共享。可分为新生代,老生代。
  • 方法区:用于存储已经被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Hotspot中的“永久代”。
  • 运行时常量池:方法区的一部分,存储常量信息,如各种字面量、符号引用等。
  • 直接内存:并不是JVM运行时数据区的一部分, 可直接访问的内存, 比如NIO会用到这部分。

按照JVM规范,除了程序计数器不会抛出OOM外,其他各个内存区域都可能会抛出OOM。

最常见的OOM情况有以下三种:

  • java.lang.OutOfMemoryError: Java heap space ——>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。

  • java.lang.OutOfMemoryError: PermGen space ——>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。

  • java.lang.StackOverflowError ——> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

各种触发OOM的方式

  1. OOM:Java heap Space:在循环中不断地创建新的对象 -Xms20m -Xmx20m
  2. 虚拟机栈和本地方法栈:无限制递归、不断创建线程执行方法
  3. 方法区和常量池:不断执行String::intern()、使用CGlib不断创建类 -XX:MaxMetaspaceSize -XX:MaxPermSize
  4. 本机直接内存溢出:Usafe不断直接分配内存空间 -XX:MaxDirectMemorySize

4.OOM分析–heapdump

要dump堆的内存镜像,可以采用如下两种方式:

  • 设置JVM参数-XX:+HeapDumpOnOutOfMemoryError,设定当发生OOM时自动dump出堆信息。不过该方法需要JDK5以上版本。
  • 使用JDK自带的jmap命令。”jmap -dump:format=b,file=heap.bin “ 其中pid可以通过jps获取。

dump堆内存信息后,需要对dump出的文件进行分析,从而找到OOM的原因。常用的工具有:

这个链接:http://www.ibm.com/developerworks/cn/opensource/os-cn-ecl-ma/index.html中提供了一个采用mat分析的例子

注意:因为JVM规范没有对dump出的文件的格式进行定义,所以不同的虚拟机产生的dump文件并不是一样的。在分析时,需要针对不同的虚拟机的输出采用不同的分析工具(当然,有的工具可以兼容多个虚拟机的格式)。IBM HeapAnalyzer也是分析heap的一个常用的工具。

5.小结

涉及到的虚拟机的技术或者工具,往往需要考虑到虚拟机规范以及不同的虚拟机实现。尤其是针对虚拟机调优时,往往需要针对虚拟机在某些方面的实现策略来考虑,比如,不同的虚拟机的垃圾回收算法是不一样的,而这直接影响了虚拟机某些参数的设置,以达到虚拟机的最佳性能。

而针对JVM运行时的分析与诊断,则需要掌握分析基本方法,针对具体情况,运用虚拟机的原理,具体分析。一句话,水很深啊。

6. Mat使用简介

MAT,全称Memory Analysis Tools,是一款分析Java堆内存的工具,可以快速定位到堆内泄漏问题。该工具提供了两种使用方式,一种是插件版,可以安装到Eclipse使用,另一种是独立版,可以直接解压使用。