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

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

2016-03-30 20:11 281 查看
一、内存区域划分



1、程序计数器:

Program Counter Register是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 每个线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2、Java虚拟机栈:

也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候会同时创建一个栈帧(Stack
Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。如果线程请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError。如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存时会抛出OutOfMemoryError

3、本地方法栈:

Native Method Stacks与虚拟机栈所发挥的作用是非常相似的,只不过一个是执行Java方法,一个是Native方法,sun公司的HotSpot虚拟机直接将两者合二为一了。

什么是Native Method(本地方法)?

           一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。

为什么要用Native Method?

   因为与java环境外交互:
   
   有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

 4、Java堆:

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。虚拟机规范中讲:所有对象的实例以及数组都要在堆上分配。Java堆是垃圾收集器管理的主要区域,很多时候称为GC堆。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError

5、方法区:

也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息(如类名),常量,静态变量,及时编译期编译后的代码(类方法)等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。

6、运行时常量池:

它包含两大类常量:字面量和符号引用,字面量包含文本字符串,声明为final的常量值等,符号引用包含类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

二、对象的创建

对于面向对象的一门语言,我们无时不在通过new关键字创建对象,那么这个过程又是怎样的呢?当虚拟机遇到一条new指令的时候,首先会去检查所new的类是否已经被加载,在哪里检查?当然在方法区,方法区存放了加载过的类信息。如果没有加载,那么先执行类的加载。通过类加载检查后,虚拟机开始为新生对象分配内存,对象所需要的内存大小在类加载完成后已经可以确定,这时候只要在堆中分配空间即可。分配内存有两种方式,第一种,我们假设内存绝对规整,那么只要在用过的内存和没用过的内存间放置一个指针即可,每次分配空间的时候只要把指针向空闲空间移动相应距离即可。第二种,我们假设空闲内存和非空闲内存夹杂在一起,实际上就是这种情况,那么就需要一个列表,去记录堆内存的使用情况,操作系统对内存的管理就是这样的。


三、对象的内存布局

对象在堆中的布局分为三个区域:对象头,实例数据,对齐填充。

对象头 包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位,MonirGC次数,哈希码,锁状态,哪个线程可以拥有等被称为
MarkWord
(标记字)。第二部分存放指向方法区类数据的指针。在32位系统中,class指针大小为4字节,标记字大小为4字节。在64位系统中标记字大小为8字节。

实例数据 存放类的属性信息,包括父类的属性信息。数组的实例部分还包括数组的长度。实例信息按类分别4字节对齐。

对齐填充 这是虚拟机要求对象起始地址必须是8字节的整数倍,可以说对齐填充没有什么特别的含义。

四、对象的访问定位

我们知道,引用是引用,对象实例是对象实例。引用存放在虚拟机栈中,数据类型为reference,对象实例存放在堆中。那么引用是如何指向对象实例的呢?

主流的访问方式有两种,第一种是通过
句柄池
,如果使用句柄池,那么
java堆
中将会划分出一部分内存作为句柄池,句柄包含对象类型指针指向方法区的类型信息,还有对象实例指针,指向堆中的实例地址。

第二种是reference引用直接指向堆中的对象实例,对象实例的对象头存放对象类型指针。

两种方法各有优势,第一种可以在对象实例在
GC
时移动的时候只改变句柄池中的对象实例指针,而不用改变reference引用本身。第二种方法就是访问速度快,减少了一次指针定位的时间开销。目前
HotSpot虚拟机
就采用的第二种方式。





五、内存溢出(注意与内存泄露相区别):

1、堆溢出

Java堆用于存储对象实例,我们只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。

List<OOMObject> list = new ArrayList<OOMObject>();
long i = 1;
while(true) {
list.add(new OOMObject("IpConfig..." + i++));
}
2、虚拟机栈和本地方法栈溢出

例如:递归调用没有停止

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


3、方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述符、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。比如动态代理会生成动态类。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: