内存区域

Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域。Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。下面详细阐述各数据区所存储的数据类型。

(图片来自:http://blog.csdn.net/ns_code/article/details/17565503

程序计数器(Program Counter Register)

一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。

当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法(调用本地操作系统方法)时,该计数器的值为空。另外,该内存区域是唯一一个在Java虚拟机规范中么有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。

Java虚拟机栈(Java Virtual Machine Stacks)

该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

  1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。

  2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这里有一点要重点说明,在多线程情况下,给每个线程的栈分配的内存越大,越容易产生内存溢出异常。操作系统为每个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值,忽略掉程序计数器消耗的内存(很小),以及进程本身消耗的内存,剩下的内存便给了虚拟机栈和本地方法栈,每个线程分配到的栈容量越大,可以建立的线程数量自然就越少。因此,如果是建立过多的线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和每个线程的栈容量来换取更多的线程。当由于创建过量线程发生OOM时,会报错:java.lang.OutOfMemoryError, unable to create new native thread。

本地方法栈(Native Method Stacks)

该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError与OutOfMemoryError异常。

Java堆(Java Heap)

Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域,几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。

根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。

注意:随着JIT编译器的发展与逃逸技术逐渐成熟,所有对象都分配在堆上也逐渐变得不是那么绝对了,线程共享的Java堆中也可能划分出线程私有的分配缓冲区(TLAB)。

方法区(Method Area)

方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java Heap一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。不过,这部分区域的回收是有必要的,如果这部分区域永远不回收,那么类型就无法卸载,我们就无法加载更多的类,HotSpot的该区域有实现垃圾回收。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

直接内存(Direct Memory) 

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。

当使用超过虚拟机允许的直接内存时,虚拟机会抛出OutOfMemoryError异常,由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。一般来说,如果发现OOM后Dump文件很小,那就应该考虑一下,是不是这块内存发生了溢出。

内存溢出

Java堆内存溢出

public class HeapOOM {
static class OOMObject {
} public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>(); while (true) {
list.add(new OOMObject());
}
}
}

  运行以上代码时,可以增加运行参数-Xms20m -Xmx20m,该参数限制Java堆大小为20M,不可扩展。运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at HeapOOM.main(HeapOOM.java:17)

  可以看到,在堆内存溢出时,除了会报错java.lang.OutOfMemoryError外,还会跟着进一步提示Java heap space。

虚拟机栈和本地方法栈溢出

要让虚拟机栈内存溢出,我们可以使用递归调用:因为每次方法调用都需要向栈中压入调用信息,当栈的大小固定时,过深的递归将向栈中压入过量信息,导致StackOverflowError:

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:10828
Exception in thread "main" java.lang.StackOverflowError
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)

可以看到,在我的电脑上运行以上代码,最多支持的栈深度是10828层,当发生栈溢出时,会报错java.lang.StackOverflowError。

方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、字段描述等,对于这个区域的测试,基本思路是运行时使用CGLib产生大量的类去填充方法区,直到溢出:

public class JavaMethodAreaOOM {
static class OOMObject {
}
public static void main(String[] args) {
while( true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject. class);
enhancer.setUseCache( false);
enhancer.setCallback( new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
}

  运行时增加虚拟机参数:-XX:PermSize=10M -XX:MaxPermSize=10M,限制永久代大小为10M,最后报错为java.lang.OutOfMemoryError: PermGen space。报错信息明确说明,溢出区域为永久代。

总结

本文主要说明Java虚拟机一共分为哪几块内存区域,以及这几块内存区域是否会内存溢出,如果这些区域发生内存溢出报错如何。了解这些知识后,以后遇到内存溢出报错,我们就可以定位到具体内存区域,然后具体问题,具体分析。

参考

  • 《深入理解Java虚拟机》

Java虚拟机——Java内存区域与内存溢出的更多相关文章

  1. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  2. 【深入Java虚拟机】之一:Java内存区域与内存溢出

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17565503 内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若 ...

  3. 《深入理解Java虚拟机》-----第2章 Java内存区域与内存溢出异常

    2.1 概述 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任 ...

  4. 【深入Java虚拟机(1)】:Java内存区域与内存溢出

    原文出处: 兰亭风雨 内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域.Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器.Jav ...

  5. 《深入理解java虚拟机》第二章 Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 2.2 运行时数据区域  

  6. 读书笔记,《深入理解java虚拟机》,第二章,java内存区域与内存溢出异常

    第二节,运行时数据区域.    在这个章节中,作者给出了一个java虚拟机运行时数据区的框图,图的左侧是方法区和堆,这两个数据区是所有的线程所共享的.然后是虚拟机栈.本地方法栈.还有程序计数器,这三个 ...

  7. 深入了解Java虚拟机(1)java内存区域与内存溢出异常

    java内存区域与内存溢出异常 一.运行时数据区域 1.程序计数器:线程私有,用于存储当前所执行的指令位置 2.Java虚拟机栈:线程私有,描叙Java方法执行模型:执行方法时都会创建一个栈帧,存储局 ...

  8. 《深入理解Java虚拟机》笔记--第二章、Java内存区域与内存溢出异常

    Java程序员把内存的控制权交给了Java虚拟机.在Java虚拟机内存管理机制的帮助下,程序员不再需要为每一个new操作写对应的delete/free代码,而且不容易出现内存泄露和溢出. 虚拟机在执行 ...

  9. 深入理解java虚拟机---->java内存区域与内存溢出异常

    2. java内存区域于内存溢出异常 2.1 概述: 对于C/C++而言,内存管理具有最高的权利,既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到结束的维护责任. 对于java而言,则把内存 ...

  10. java虚拟机(一)-java内存区域与内存溢出异常

    1.简述:java虚拟机在执行java程序的过程中,会把他所管理的内存分为以下几个区域, 1.1.程序计数器 1.2.虚拟机栈 1.3.本地方法栈 1.4.java堆 1.5.方法区 如图所示: 2. ...

随机推荐

  1. TPS、QPS和系统吞吐量的区别和理解

    参考:https://blog.csdn.net/u010889616/article/details/83245695 一.QPS/TPSQPS:Queries Per Second意思是“每秒查询 ...

  2. python类对象属性查找原理

    class Foo(object): def __init__(self): # 这是一个对象属性 self.obj_pro = 12 # 这是一类属性 c_pro = 11 # 这是一个静态方法 @ ...

  3. ORACLE修改表字段顺序

    1.若'GYZL_BZPWL_TB' 为要修改的表名,注意表名一定要大写!此句可以获取表的id.select object_id from all_objects where object_name= ...

  4. jQuery入门教程-文档操作方法

    一.append()和appendTo() 1.1 append()方法 <body> <p>好好学习</p> <button>append() 方法& ...

  5. poj3744 (概率DP+矩阵快速幂)

    http://poj.org/problem?id=3744 题意:在一条铺满地雷的路上,你现在的起点在1处.在N个点处布有地雷,1<=N<=10.地雷点的坐标范围:[1,10000000 ...

  6. oracle blob 反序列化错误

    代码的目的是先将一个配置类JobConfig序列化存进Oracle中的Blob中,然后查的时候反序列化出来. 先看一下控制台报错 ### Cause: com.audaque.lib.core.exc ...

  7. js中return;、return true、return false的区别

    一.返回控制与函数结果, 语法为:return 表达式; 语句结束函数执行,返回调用函数,而且把表达式的值作为函数的结果  二.返回控制, 无函数结果,语法为:return;  在大多数情况下,为事件 ...

  8. 本站CSS代码

    body { /*字体样式*/ font-family: "youyuan",幼圆,"MicrosoftJhengHei",华文细黑,STHeiti,MingL ...

  9. PHP 数组下标自动转换为整型的坑

    在做项目时,上线后遇到一个 BUG,有一个数组存储了下标从 '01'到'18' 总共18组数据.上线前测试了前几组数据,没问题.上线后,在用户选择'15'时报错,找不到这个数据.查了一下代码,数据是没 ...

  10. 运用php函数mkdir创建多级目录

    php默认的mkdir一次只能创建一层目录,如果在当前目录下创建一个div/css/layout 的目录就需要逐层逐层的先创建div,再创建div/css 再创建 div/css/layout,然而我 ...