一、内存区域划分
1、程序计数器:
Program Counter Register是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。 每个线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2、Java虚拟机栈:
也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。每个方法被执行的时候会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。如果线程请求栈深度大于虚拟机所允许的深度,抛出StackOverflowError。如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存时会抛出OutOfMemoryError
Native Method Stacks与虚拟机栈所发挥的作用是非常相似的,只不过一个是执行Java方法,一个是Native方法,sun公司的HotSpot虚拟机直接将两者合二为一了。
什么是Native Method(本地方法)?
一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。
为什么要用Native Method?
因为与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的相关信息,如类名、访问修饰符、常量池、字段描述符、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。比如动态代理会生成动态类。