JVM内存结构及分析

JVM内存模型

名称特征作用配置参数异常程序计数器占用内存小,线程私有,
生命周期与线程相同大致为字节码行号指示器无无虚拟机栈线程私有,生命周期与线程相同,使用连续的内存空间Java 方法执行的内存模型,存储局部变量表、操作栈、动态链接、方法出口等信息-XssStackOverflowError
OutOfMemoryErrorjava堆线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址保存对象实例,所有对象实例(包括数组)都要在堆上分配-Xms
-Xsx
-XmnOutOfMemoryError方法区线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:
16M
-XX:MaxPermSize
64MOutOfMemoryError运行时常量池方法区的一部分,具有动态性存放字面量及符号引用
1.1 程序计数器
程序 计数器( Program Counter Register) 是一 块 较小 的 内存 空间, 它的 作用 可以 看做 是 当前 线程 所 执行 的 字节 码 的 行号 指示器。 在 虚拟 机 的 概念 模型 里( 仅是 概念 模型, 各种 虚拟 机 可能 会 通过 一些 更 高效 的 方式 去 实现), 字节 码 解释器 工作 时 就是 通过 改变 这个 计数器 的 值 来 选取 下一 条 需要 执行 的 字节 码 指令, 分支、 循环、 跳 转、 异常 处理、 线程 恢复 等 基础 功能 都 需要 依赖 这个 计数器 来 完成。
由于 Java 虚拟 机 的 多 线程 是 通过 线程 轮流 切换 并 分配 处理器 执行 时间 的 方式 来 实现 的, 在任 何 一个 确定 的 时刻, 一个 处理器( 对于 多 核 处理器 来说 是一 个 内核) 只会 执行 一条 线程 中的 指令。 因此, 为了 线程 切换 后能 恢复 到 正确 的 执行 位置, 每条 线程 都 需要 有一个 独立 的 程序 计数器, 各条 线程 之间 的 计数器 互不 影响, 独立 存储, 我们 称 这类 内存 区域 为“ 线程 私有” 的 内存。 如果 线程 正在 执行 的 是 一个 Java 方法, 这个 计数器 记录 的 是 正在 执行 的 虚拟 机 字节 码 指令 的 地址; 如果 正在 执 行的 是 Natvie 方法, 这个 计数器 值 则为 空( Undefined)。 此 内存 区域 是 唯一 一个 在 Java 虚拟 机 规范 中 没有 规定 任何 OutOfMemoryError 情况 的 区域。

1.2 Java 虚拟 机 栈
与 程序 计数器 一样, Java 虚拟 机 栈( Java Virtual Machine Stacks) 也是 线程 私有 的, 它的 生命 周期 与 线程 相同。 虚拟 机 栈 描述 的 是 Java 方法 执行 的 内存 模型: 每个 方法 被 执行 的 时候 都会 同时 创建 一个 栈 帧( Stack Frame[ 1]) 用于 存储 局部 变 量表、 操作 栈、 动态 链接、 方法 出口 等 信息。 每一个 方法 被 调用 直至 执行 完成 的 过程, 就 对应 着 一个 栈 帧 在 虚拟 机 栈 中 从 入栈 到 出 栈 的 过程。 经常 有人 把 Java 内存 区 分为 堆 内存( Heap) 和 栈 内存( Stack), 这种 分法 比较 粗糙, Java 内存 区域 的 划分 实际上 远比 这 复杂。 这种 划分 方式 的 流行 只能 说明 大多数 程序员 最 关注 的、 与 对象 内存 分配 关系 最 密切 的 内存 区域 是 这 两块。 其中 所指 的“ 堆” 在后面 会 专门 讲述, 而 所指 的“ 栈” 就是 现在 讲的 虚拟 机 栈, 或者 说是 虚拟 机 栈 中的 局部 变量 表 部分。 局部 变量 表 存放 了 编译 期 可知 的 各种 基本 数据 类型( boolean、 byte、 char、 short、 int、 float、 long、 double)、 对象 引用( reference 类型, 它不 等同 于 对象 本身, 根据 不同 的 虚拟 机 实现, 它可 能 是一 个 指向 对象 起始 地址 的 引用 指针, 也可能 指向 一个 代表 对象 的 句柄 或者 其他 与此 对象 相关 的 位置) 和 returnAddress 类型( 指向 了 一条 字节 码 指令 的 地址)。 其中 64 位 长度 的 long 和 double 类型 的 数据 会 占用 2 个 局部 变量 空间(Slot), 其余 的 数据 类型 只占 用 1 个。 局部 变量 表 所需 的 内存 空间 在编 译 期间 完成 分配, 当 进入 一个 方法 时, 这个 方法 需 要在 帧 中 分配 多大 的 局部 变量 空间 是 完全 确定 的, 在 方法 运行 期间 不会 改变 局部 变 量表 的 大小。 在 Java 虚拟 机 规范 中, 对这 个 区域 规定了 两种 异常 状况: 如果 线程 请求 的 栈 深度 大于 虚拟 机 所 允许 的 深度, 将 抛出 StackOverflowError 异常; 如果 虚拟 机 栈 可以 动态 扩展( 当前 大部分 的 Java 虚拟 机 都可 动态 扩展, 只不过 Java 虚拟 机 规范 中 也 允许 固定 长度 的 虚拟 机 栈), 当 扩展 时 无法 申请 到 足够 的 内存 时会 抛出 OutOfMemoryError 异常。

线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
动画是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

对于执行引擎来说,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
reference类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。
returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果是实例方法(非static),那么局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过this访问。
Slot是可以重用的,当Slot中的变量超出了作用域,那么下一次分配Slot的时候,将会覆盖原来的数据。Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
操作数栈
和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作——压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果又压回到操作数栈中,看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量的:
[plain] view plain copy
print?
  1. begin 
  2. iload_0    // push the int in local variable 0 ontothe stack 
  3. iload_1    //push the int in local variable 1 onto the stack 
  4. iadd       // pop two ints, add them, push result 
  5. istore_2   // pop int, store into local variable 2 
  6. end 

在这个字节码序列里,前两个指令iload_0和iload_1将存储在局部变量中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。下图详细表述了这个过程中局部变量和操作数栈的状态变化,图中没有使用的局部变量区和操作数栈区域以空白表示。





动态连接
虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用。如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法。
如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
返回地址
       方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法。
不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,,果是因为异常退出的,则是需要通过异常处理表来确定。
方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括:恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。





异常
在Java 虚拟机规范中,对虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

package com.sf.jvm;

/**
 * VM Args:- Xss128k
 */
public class JavaVMStackSOF {
    private int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println(" stack length:" + oom.stackLength);
            throw e;
        }
    }
}

 stack length:22337
Exception in thread "main" java.lang.StackOverflowError
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
	at com.sf.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)


1.3 本地 方法 栈

本地 方法 栈( Native Method Stacks) 与 虚拟 机 栈 所 发挥 的 作用 是非 常 相似 的, 其 区别 不过 是 虚拟 机 栈 为 虚拟 机 执行 Java 方法( 也就是 字节 码) 服务, 而 本地 方法 栈 则是 为 虚拟 机 使 用到 的 Native 方法 服务。 虚拟 机 规范 中 对本 地 方法 栈 中的 方法 使用 的 语言、 使用 方式 与 数据 结构 并没有 强制 规定, 因此 具体 的 虚拟 机 可以 自由 实现 它。 甚至 有的 虚拟 机( 譬如 Sun HotSpot 虚拟 机) 直接 就把 本地 方法 栈 和 虚拟 机 栈 合二为一。 与 虚拟 机 栈 一样, 本地 方法 栈 区域 也会 抛出 StackOverflowError 和 OutOfMemoryError 异常。

对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。
  本地方法本质上时依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法。
  任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。
  如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。
  很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。
  下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。
  这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。 



该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
注意   特别 提示 一下, 如果 读者 要 尝试 运行 上面 这段 代码, 记得 要 先 保存 当前 的 工作, 由于 在 Windows 平台 的 虚拟 机中, Java 的 线程 是 映射 到 操作系统 的 内核 线程 上 的[ 2], 所以 上述 代码 执行 时有 较大 的 风险, 可能 会 导致 操作系统 假死。

package com.sf.jvm;

/**
 * VM Args:
 * -Xss2M
 */
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void stackLeakByThread() {
        int threadNum = 0;
        while (true) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    dontStop();
                }
            });
            thread.start();
            threadNum++;

        }
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
Exception in thread "main" java. lang. OutOfMemoryError: unable to create new native thread


1.4 Java 堆
对于 大多数 应用 来说, Java 堆( Java Heap) 是 Java 虚拟 机 所 管理 的 内存 中最 大的 一块。 Java 堆 是 被 所有 线程 共享 的 一块 内存 区域, 在 虚拟 机 启动 时 创建。 此 内存 区域 的 唯一 目的 就是 存放 对象 实例, 几乎 所有 的 对象 实例 都在 这里 分配 内存。 这一 点在 Java 虚拟 机 规范 中的 描述 是: 所有 的 对象 实例 以及 数组 都 要在 堆 上 分配[ 2], 但是 随着 JIT 编译器 的 发展 与 逃逸 分析 技术 的 逐渐 成熟, 栈 上 分配、 标量 替换[ 3] 优化 技术 将会 导致 一些 微妙 的 变化 发生, 所有 的 对象 都 分配 在 堆 上 也 渐渐 变得 不是 那么“ 绝对” 了。 Java 堆 是 垃圾 收集 器 管理 的 主要 区域, 因此 很多 时候 也 被 称做“ GC 堆”( Garbage Collected Heap, 幸好 国内 没 翻译 成“ 垃圾堆”)。 如果 从内 存 回收 的 角度 看, 由于 现在 收集 器 基本 都是 采 用的 分 代收 集 算法, 所以 Java 堆 中 还可以 细分 为: 新生代 和 老 年代; 再 细致 一点 的 有 Eden 空间、 From Survivor 空间、 To Survivor 空间 等。 如果 从内 存 分配 的 角度 看, 线程 共享 的 Java 堆 中 可能 划分 出 多个 线程 私有 的 分配 缓冲区( Thread Local Allocation Buffer, TLAB)。 不过, 无论如何 划分, 都与 存放 内容 无关, 无论 哪个 区域, 存储 的 都 仍然是 对象 实例, 进一步 划分 的 目的 是 为了 更好 地 回收 内存, 或者 更快 地 分配 内存。 在 本章 中, 我们 仅仅 针对 内存 区域 的 作用 进行 讨论, Java 堆 中的 上述 各个 区域 的 分配 和 回收 等 细节 将会 是 下 一章 的 主题。 根据 Java 虚拟 机 规范 的 规定, Java 堆 可以 处于 物理上 不连续 的 内存 空间 中, 只要 逻辑上 是 连续 的 即可, 就 像 我们 的 磁盘 空间 一样。 在 实现 时, 既可以 实现 成 固定 大小 的, 也可以 是 可扩展 的, 不过 当前 主流 的 虚拟 机 都是 按照 可扩展 来 实现 的( 通过- Xmx 和- Xms 控制)。 如果 在 堆 中 没有 内存 完成 实例 分配, 并且 堆 也 无法 再 扩展 时, 将会 抛出 OutOfMemoryError 异常。

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致为:

从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
GC 堆
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。
当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳
( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

设置 JVM 参数为 -XX:+PrintGCDetails,使得控制台能够显示 GC 相关的日志信息,执行上面代码,下面是其中一次执行的结果。

详细参数解析
-Xms初始堆大小。如:-Xms256m-Xmx最大堆大小。如:-Xmx512m-Xmn新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%-XssJDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。-XX:NewRatio新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3-XX:SurvivorRatio新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10-XX:PermSize永久代(方法区)的初始大小-XX:MaxPermSize永久代(方法区)的最大值-XX:+PrintGCDetails打印 GC 信息-XX:+HeapDumpOnOutOfMemoryError让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

Java 堆 用于 储存 对象 实例, 我们 只要 不断 地 创建 对象, 并且 保证 GC Roots 到 对象 之间 有可 达 路径 来 避免 垃圾 回收 机制 清除 这些 对象, 就会 在 对象 数量 到达 最 大堆 的 容量 限制 后 产生 内存 溢出 异常。 代码 如下 中 限制 Java 堆 的 大小 为 20MB, 不可 扩展( 将 堆 的 最小值- Xms 参数 与 最大值- Xmx 参数 设置 为 一样 即可 避免 堆 自动 扩展), 通过 参数- XX:+ HeapDump OnOutOfMemoryError 可以 让 虚拟 机 在 出现 内存 溢出 异常 时 Dump 出 当前 的 内存 堆 转储 快照 以便 事后 进行 分析.

package com.sf.jvm;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:
 * -Xms20m -Xmx20m  
 * -XX:+HeapDumpOnOutOfMemoryError
 * -XX:+PrintGCDetails
 */
public class HeapOutOfMemory {

    public static void main(String[] args) {
        outOfMemory();
    }
    static void noOutOfMemory(){

        while (true) {
            new OOMObject();
        }
    }
    static void outOfMemory(){
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
class OOMObject {
    byte mem[] = new byte[2014];
}
异常信息

[GC (Allocation Failure) [PSYoungGen: 5632K->512K(6144K)] 5632K->5024K(19968K), 0.0027725 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 6144K->504K(6144K)] 10656K->11432K(19968K), 0.0022202 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 504K->0K(6144K)] [ParOldGen: 10928K->10092K(13824K)] 11432K->10092K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0149535 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->2040K(6144K)] [ParOldGen: 10092K->13626K(13824K)] 15724K->15666K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0156117 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5510K(6144K)] [ParOldGen: 13626K->13626K(13824K)] 19258K->19136K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0108812 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5630K(6144K)] [ParOldGen: 13626K->13626K(13824K)] 19258K->19256K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0062819 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13626K->13626K(13824K)] 19258K->19258K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0052887 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13690K->13690K(13824K)] 19322K->19322K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0054803 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13807K->13770K(13824K)] 19439K->19402K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0054214 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13822K->13822K(13824K)] 19454K->19454K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0288703 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13822K->13807K(13824K)] 19454K->19439K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0141432 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13819K->13819K(13824K)] 19451K->19451K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0072277 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13823K->13823K(13824K)] 19455K->19455K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0082125 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 5632K->5632K(6144K)] [ParOldGen: 13823K->13823K(13824K)] 19455K->19455K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0086009 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2780.hprof ...
Heap dump file created [20898436 bytes in 0.027 secs]
[Full GC (Ergonomics) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.sf.jvm.OOMObject.<init>(HeapOutOfMemory.java:29)
	at com.sf.jvm.HeapOutOfMemory.outOfMemory(HeapOutOfMemory.java:23)
	at com.sf.jvm.HeapOutOfMemory.main(HeapOutOfMemory.java:12)
[PSYoungGen: 5632K->0K(6144K)] [ParOldGen: 13823K->579K(13824K)] 19455K->579K(19968K), [Metaspace: 2973K->2973K(1056768K)], 0.0096016 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 6144K, used 177K [0x00000000ff980000, 0x0000000100000000, 0x0000000100000000)
  eden space 5632K, 3% used [0x00000000ff980000,0x00000000ff9ac4a0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 13824K, used 579K [0x00000000fec00000, 0x00000000ff980000, 0x00000000ff980000)
  object space 13824K, 4% used [0x00000000fec00000,0x00000000fec90c20,0x00000000ff980000)
 Metaspace       used 3005K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 326K, capacity 388K, committed 512K, reserved 1048576K


用jvisualvm.exe打开查看dump,发现主要是OOMobject不被释放。如下




1.5 方法 区
方法 区( Method Area) 与 Java 堆 一样, 是 各个 线程 共享 的 内存 区域, 它 用于 存储 已被 虚拟 机 加载 的 类 信息、 常量、 静态 变量、 即时 编译器 编译 后的 代码 等 数据。 虽然 Java 虚拟 机 规范 把 方法 区 描述为 堆 的 一个 逻辑 部分, 但是 它 却有 一个 别名 叫做 Non- Heap( 非 堆), 目的 应该 是与 Java 堆 区分 开来。 对于 习惯 在 HotSpot 虚拟 机上 开发 和 部署 程序 的 开发者 来说, 很多人 愿意 把 方法 区 称为“ 永久 代”( Permanent Generation), 本质上 两者 并不 等价, 仅仅 是因为 HotSpot 虚拟 机 的 设计 团队 选择 把 GC 分 代收 集 扩展 至 方法 区, 或者说 使用 永久 代 来 实现 方法 区 而已。 对于 其他 虚拟 机( 如 BEA JRockit、 IBM J9 等) 来说 是 不存在 永久 代 的 概念 的。 即使是 HotSpot 虚拟 机 本身, 根据 官方 发布 的 路线 图 信息, 现在 也有 放弃 永久 代 并“ 搬家” 至 Native Memory 来 实现 方法 区 的 规划 了。 Java 虚拟 机 规范 对这 个 区域 的 限制 非常 宽松, 除了 和 Java 堆 一样 不需要 连续 的 内存 和 可以 选择 固定 大小 或者 可扩展 外, 还可以 选择 不实 现 垃圾 收集。 相对而言, 垃圾 收集 行为 在这 个 区域 是 比较 少 出现 的, 但 并非 数据 进入 了 方法 区 就 如 永久 代 的 名字 一样“ 永久” 存在 了。 这个 区域 的 内存 回收 目标 主要 是 针对 常量 池 的 回收 和 对 类型 的 卸载, 一般来说 这个 区域 的 回收“ 成绩” 比较 难以 令人满意, 尤其是 类型 的 卸载, 条件 相当 苛刻, 但是 这部 分 区域 的 回收 确实 是有 必要 的。 在 Sun 公司 的 BUG 列表 中, 曾 出现 过 的 若干个 严重 的 BUG 就是 由于 低 版本 的 HotSpot 虚拟 机 对此 区域 未完 全 回收 而 导致 内存 泄漏。 根据 Java 虚拟 机 规范 的 规定, 当 方法 区 无法 满足 内存 分配 需求 时, 将 抛出 OutOfMemoryError 异常。


方法 区 用于 存放 Class 的 相关 信息, 如 类 名、 访问 修饰 符、 常量 池、 字段 描述、 方法 描述 等。 对于 这个 区域 的 测试, 基本 的 思路 是 运行时 产生 大量 的 类 去 填满 方法 区, 直到 溢出。 虽然 直接 使用 Java SE API 也可以 动态 产生 类( 如 反射 时 的 GeneratedConstructorAccessor 和 动态 代理 等), 但在 本次 实验 中 操作 起来 比较 麻烦。 在 代码 清单 2- 5 中, 笔者 借助 CGLib[ 3] 直接 操作 字节 码 运行时, 生成 了 大量 的 动态 类。 值得 特别 注意 的 是, 我们 在这 个 例子 中 模拟 的 场景 并非 纯粹 是一 个 实验, 这样 的 应用 经常 会 出现 在 实际 应用 中: 当前 的 很多 主流 框架, 如 Spring 和 Hibernate 对 类 进行 增强 时, 都会 使用 到 CGLib 这类 字节 码 技术, 增 强的 类 越多, 就 需要 越大 的 方法 区 来 保证 动态 生成 的 Class 可以 加载 入 内存。
代码 清单 2- 5   借助 CGLib 使得 方法 区 出现 内存 溢出 异常
package com.sf.jvm;

/**
 * VM Args: -XX: PermSize= 10M -XX: MaxPermSize= 10M
 */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {

                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}
运行 结果: 
Caused by: java. lang. OutOfMemoryError: PermGen space at java. lang. ClassLoader. defineClass1( Native Method) at java. lang. ClassLoader. defineClassCond( ClassLoader. java: 632) at java. lang. ClassLoader. defineClass( ClassLoader. java: 616)


方法 区 溢出 也是 一种 常见 的 内存 溢出 异常, 一个 类 如果 要被 垃圾 收集 器 回收 掉, 判定 条件 是非 常 苛刻 的。 在 经常 动态 生成 大量 Class 的 应用 中, 需要 特别 注意 类 的 回收 状况。 这类 场景 除了 上面 提到 的 程序 使用 了 GCLib 字节 码 增强 外, 常见 的 还有: 大量 JSP 或 动态 产生 JSP 文件 的 应用( JSP 第一次 运行时 需要 编译 为 Java 类)、 基于 OSGi 的 应用( 即使是 同一个 类 文件, 被 不同 的 加载 器 加载 也会 视为 不同 的 类) 等。


运行时 常量 池( Runtime Constant Pool) 是 方法 区 的 一部分。 Class 文件 中 除了 有 类 的 版本、 字段、 方法、 接口 等 描述 等 信息 外, 还有 一项 信息 是 常量 池( Constant Pool Table), 用于 存放 编译 期 生成 的 各种 字面 量 和 符号 引用, 这部 分 内容 将 在 类 加载 后 存放 到 方法 区 的 运行时 常量 池 中。 Java 虚拟 机 对 Class 文件 的 每一 部分( 自然 也 包括 常量 池) 的 格式 都有 严格 的 规定, 每一个 字节 用于 存储 哪种 数据 都 必须 符合 规范 上 的 要求, 这样 才会 被 虚拟 机 认可、 装载 和 执行。 但 对于 运行时 常量 池, Java 虚拟 机 规范 没有 做 任何 细节 的 要求, 不同 的 提供 商 实现 的 虚拟 机 可以 按照 自己的 需要 来 实现 这个 内存 区域。 不过, 一般来说, 除了 保存 Class 文件 中 描述 的 符号 引用 外, 还会 把 翻译 出来 的 直接 引用 也 存储 在 运行时 常量 池 中[ 4]。 运行时 常量 池 相对于 Class 文件 常量 池 的 另外 一个 重要 特征 是 具备 动态 性, Java 语言 并不 要求 常量 一定 只能 在 编译 期 产生, 也就是 并非 预置 入 Class 文件 中 常量 池 的 内容 才能 进入 方法 区 运行时 常量 池, 运行 期间 也可 能将 新的 常量 放入 池 中, 这种 特性 被 开发 人员 利用 得比 较多 的 便是 String 类 的 intern() 方法。 既然 运行时 常量 池 是 方法 区 的 一部分, 自然 会受 到 方法 区 内存 的 限制, 当 常量 池 无法 再 申请 到 内存 时会 抛出 OutOfMemoryError 异常。
运行时 常量 池( Runtime Constant Pool) 是 方法 区 的 一部分。 Class 文件 中 除了 有 类 的 版本、 字段、 方法、 接口 等 描述 等 信息 外, 还有 一项 信息 是 常量 池( Constant Pool Table), 用于 存放 编译 期 生成 的 各种 字面 量 和 符号 引用, 这部 分 内容 将 在 类 加载 后 存放 到 方法 区 的 运行时 常量 池 中。 Java 虚拟 机 对 Class 文件 的 每一 部分( 自然 也 包括 常量 池) 的 格式 都有 严格 的 规定, 每一个 字节 用于 存储 哪种 数据 都 必须 符合 规范 上 的 要求, 这样 才会 被 虚拟 机 认可、 装载 和 执行。 但 对于 运行时 常量 池, Java 虚拟 机 规范 没有 做 任何 细节 的 要求, 不同 的 提供 商 实现 的 虚拟 机 可以 按照 自己的 需要 来 实现 这个 内存 区域。 不过, 一般来说, 除了 保存 Class 文件 中 描述 的 符号 引用 外, 还会 把 翻译 出来 的 直接 引用 也 存储 在 运行时 常量 池 中[ 4]。 运行时 常量 池 相对于 Class 文件 常量 池 的 另外 一个 重要 特征 是 具备 动态 性, Java 语言 并不 要求 常量 一定 只能 在 编译 期 产生, 也就是 并非 预置 入 Class 文件 中 常量 池 的 内容 才能 进入 方法 区 运行时 常量 池, 运行 期间 也可 能将 新的 常量 放入 池 中, 这种 特性 被 开发 人员 利用 得比 较多 的 便是 String 类 的 intern() 方法。 既然 运行时 常量 池 是 方法 区 的 一部分, 自然 会受 到 方法 区 内存 的 限制, 当 常量 池 无法 再 申请 到 内存 时会 抛出 OutOfMemoryError 异常。
package com.sf.jvm;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:- XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用 List 保持 着 常量 池 引用, 避免 Full GC 回收 常量 池 行为
        List<String> list = new ArrayList<String>();
        // 10MB 的 PermSize 在 integer 范围内 足够 产生 OOM 了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++ + "xxxxxxxxxxxxxxxxxxxxx").intern());
        }
    }
}
Exception in thread "main" java. lang. OutOfMemoryError: PermGen space at java. lang. String. intern( Native Method) at org. fenixsoft. oom. RuntimeConstantPoolOOM. main( RuntimeConstantPoolOOM. java:


1.7 直接 内存
直接 内存( Direct Memory) 并不是 虚拟 机 运行时 数据区 的 一部分, 也不 是 Java 虚拟 机 规范 中 定义 的 内存 区域, 但是 这部 分 内存 也 被 频繁 地 使用, 而且 也可能 导致 OutOfMemoryError 异常 出现, 所以 我们 放到 这里 一起 讲解。 在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入 了 一种 基于 通道( Channel) 与 缓冲区( Buffer) 的 I/ O 方式, 它可 以 使用 Native 函数 库 直接 分配 堆 外 内存, 然后 通过 一个 存储 在 Java 堆里 面的 DirectByteBuffer 对象 作为 这块 内存 的 引用 进行 操作。 这样 能在 一些 场景 中 显著 提高 性能, 因为 避免 了 在 Java 堆 和 Native 堆 中 来回 复制 数据。 显然, 本机 直接 内存 的 分配 不会 受到 Java 堆 大小 的 限制, 但是, 既然 是 内存, 则 肯定 还是 会受 到 本机 总 内存( 包括 RAM 及 SWAP 区 或者 分页 文件) 的 大小 及 处理器 寻址 空间 的 限制。 服务器 管理员 配置 虚拟 机 参数 时, 一般 会 根据 实际 内存 设置- Xmx 等 参数 信息, 但 经常 会 忽略 掉 直接 内存, 使得 各个 内存 区域 的 总和 大于 物理 内存 限制( 包括 物理上 的 和 操作系统 级 的 限制), 从而 导致 动态 扩展 时 出现 OutOfMemoryError 异常。

DirectMemory 容量 可通过- XX: MaxDirectMemorySize 指定, 如果不 指定, 则 默认 与 Java 堆 的 最大值(- Xmx 指定) 一样。  越过 了 DirectByteBuffer 类, 直接 通过 反射 获取 Unsafe 实例 并进 行 内存 分配( Unsafe 类 的 getUnsafe() 方法 限制 了 只有 引导 类 加载 器 才会 返回 实例, 也就是 设计者 希望 只有 rt. jar 中的 类 才能 使用 Unsafe 的 功能)。 因为, 虽然 使用 DirectByteBuffer 分配 内存 也会 抛出 内存 溢出 异常, 但它 抛出 异常 时并 没有 真正 向 操作系统 申请 分配 内存, 而是 通过 计算 得知 内存 无法 分配, 于是 手动 抛出 异常, 真正 申请 分配 内存 的 方法 是 unsafe. allocateMemory()。

package com.sf.jvm;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import static com.sun.deploy.util.BufferUtil.MB;
/**
 * VM Args:- Xmx20M -XX: MaxDirectMemorySize= 10M
 */
public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError
	at sun.misc.Unsafe.allocateMemory(Native Method)
	at com.sf.jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:20)


1.8 对象 访问
介绍 完 Java 虚拟 机 的 运行时 数据区 之后, 我们 就可 以来 探讨 一个 问题: 在 Java 语言 中, 对象 访问 是 如何 进行 的? 对象 访问 在 Java 语言 中 无处不在, 是最 普通 的 程序 行为, 但 即使是 最简单 的 访问, 也会 却 涉及 Java 栈、 Java 堆、 方法 区 这 三个 最重要 内存 区域 之间 的 关联 关系, 如下 面的 这句 代码: Object obj = new Object(); 假设 这句 代码 出现 在 方法 体中, 那“ Object obj” 这 部分 的 语义 将会 反映 到 Java 栈 的 本地 变量 表中, 作为 一个 reference 类型 数据 出现。 而“ new Object()” 这 部分 的 语义 将会 反映 到 Java 堆 中, 形成 一块 存储 了 Object 类型 所有 实例 数据 值( Instance Data, 对象 中 各个 实例 字段 的 数据) 的 结构 化 内存, 根据 具体 类型 以及 虚拟 机 实现 的 对象 内存 布局( Object Memory Layout) 的 不同, 这块 内存 的 长度 是 不 固定 的。 另外, 在 Java 堆 中 还 必须 包含 能 查找 到此 对象 类型 数据( 如对 象 类型、 父 类、 实现 的 接口、 方法 等) 的 地址 信息, 这些 类型 数据 则 存储 在 方法 区 中。 由于 reference 类型 在 Java 虚拟 机 规范 里面 只 规定了 一个 指向 对象 的 引用, 并没有 定义 这个 引用 应该 通过 哪种 方式 去 定位, 以及 访问 到 Java 堆 中的 对象 的 具体 位置, 因此 不同 虚拟 机 实现 的 对象 访问 方式 会 有所不同, 主流 的 访问 方式 有 两种: 使用 句柄 和 直接 指针。 ·如果 使用 句柄 访问 方式, Java 堆 中将 会 划分 出 一块 内存 来作 为 句柄 池, reference 中 存储 的 就是 对象 的 句柄 地址, 而 句柄 中 包含 了 对象 实例 数据 和 类型 数据 各自 的 具体 地址 信息, 如图 2- 2 所示。 图 2- 2   通过 句柄 访问 对象 ·如果 使用 直接 指针 访问 方式, Java 堆 对象 的 布局 中就 必须 考虑 如何 放置 访问 类型 数据 的 相关 信息, reference 中直 接 存储 的 就是 对象 地址, 如图 2- 3 所示。 图 2- 3   通过 直接 指针 访问 对象 这 两种 对象 的 访问 方式 各有 优势, 使用 句柄 访问 方式 的 最大 好处 就是 reference 中 存储 的 是 稳定 的 句柄 地址, 在 对象 被 移动( 垃圾 收集 时 移动 对象 是非 常 普遍 的 行为) 时 只会 改变 句柄 中的 实例 数据 指针, 而 reference 本身 不需 要被 修改。 使用 直接 指针 访问 方式 的 最大 好处 就是 速度 更快, 它 节省 了 一次 指针 定位 的 时间 开销, 由于 对象 的 访问 在 Java 中非 常 频繁, 因此 这类 开销 积少成多 后 也是 一项 非常 可观 的 执行 成本。 就 本书 讨论 的 主要 虚拟 机 Sun HotSpot 而言, 它是 使用 第二 种 方式 进行 对象 访问 的, 但 从 整个 软件 开发 的 范围 来看, 各种 语言 和 框架 使用 句柄 来 访问 的 情况 也 十分 常见。


参照:
深入理解Java虚拟机
http://blog.csdn.net/u012152619/article/details/46968883
http://www.importnew.com/14630.html

猜你喜欢

转载自wuhaocn.iteye.com/blog/2384655