深刻学习Java虚拟机——虚拟机内存区域与内存溢出异常

1. 运行时数据区域

 

1.1 程序计数器

    1. 程序计数器是一段较小的内存空间,能够看做为当前线程所执行字节码的行号指示器。经过改变这个计数器的值来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要该计数器。程序员

    2. 每条线程都会有一个独立的程序计数器,各线程间程序计数器互不影响,独立存储,因此这个内存区域是线程私有的算法

    3. 若是线程正在执行的是一个Java方法,那么计数器记录的是正在执行的虚拟机字节码指令的地址,若是执行的是本地(Native)方法,则这个计数器值为空,此内存区域是惟一一个在Java虚拟机中没有OutOfMemoryError状况的区域数组

1.2 虚拟机栈

    1. 首先,虚拟机栈是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法的执行模型:每一个方法在执行的同时都会建立一个栈桢,用于存储局部变量表,操做数栈,动态连接,方法出口等信息。每个方法从调用直至执行完成,就对应着一个栈桢从入栈到出栈的过程。安全

    2. 局部变量表存放了编译器可知的各类基本数据类型(boolean,byte,char,short,int,long,float,double)、对象引用类型和returnAddress类型(指向一条字节码指令的地址)。且局部变量表的内存空间会在编译期完成分配,方法运行期间不会改变局部变量表的大小。数据结构

    3. 异常情况:并发

(1)若是线程请求的栈深度大于虚拟机所容许的深度,则将抛出StackOverflowError异常函数

(2)若是虚拟机栈能够动态扩展,而扩展时没法申请的足够的内存,就会抛出OutOfMemoryError异常布局

1.3 本地方法栈

    1. 本地方法栈与虚拟机栈相似,但虚拟机栈是为虚拟机执行Java方法服务的,而本地方法栈是为虚拟机使用的本地方法服务。性能

    2. 一样的,该内存区域也会有StackOverflowError异常和OutOfMemoryError异常。spa

1.4 Java堆

    1. Java堆是被全部线程共享的内存区域,在虚拟机启动时建立。此区域只用来存储对象实例,几乎全部的对象都会在这里被建立(并非全部的对象都在堆中建立)。

    2. Java堆是垃圾收集器管理的主要区域。从内存回收角度看,垃圾收集器主要采用分代收集算法,因此还能够将Java堆分为新生代和老年代,进一步细分为Eden区,From Survivor区和To Survivor区。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。

    进行这些划分的目的都是为了更快更好的回收内存或者分配内存

    3. 可能发生的异常:Java堆可能会处于物理上内存空间不连续的内存空间中,但逻辑上必须是连续的。其空间大小能够经过-Xmx和-Xms来控制,能够实现为固定大小,也能够为可扩展大小。当没有足够的内存空间完成分配而且堆没法扩展时,就会抛出OutOfMemoryError异常

1.5 方法区

    1. 方法区是线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,方法区属于堆的一个逻辑部分,但它却仍然要与Java堆区分开。

    2. 方法区与堆相似,不须要连续的内存空间、内存空间大小能够固定或者可扩展,还能够选择不实现垃圾收集。在方法区,不多出现垃圾收集,这个区域的内存回收主要针对常量池的回收以及对类型的卸载。

    3. 当方法区没法知足内存分配需求时,就会抛出OutOfMemoryError异常

    4. 运行时常量池:该区域是方法区的一部分,Class文件中除了有相似的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各类字面量和符号引用,这部份内容将在类被加载后进入方法区的运行时常量池中存放。运行时常量池相对于Class文件常量池的一个重要特征是具有动态性,即运行期间也可能将新的常量放入运行时常量池,好比String类中的intern()方法。

1.6 直接内存

    1.直接内存并非虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

    2.应用:在jdk1.4之后加入了NIO类,引入了一种基于通道与缓冲区的新IO方式,它使用Native函数库直接分配堆外内存,而后经过一个存储在Java堆中DirectByteBuffrer对象做为这块内存的引用进行直接操做,避免了在Java堆与Native堆之间来回复制数据,显著提升了性能。因为分配的是Native内存空间,因此大小不会受到Java堆大小的限制,可是确定会受到本机总内存的限制。在经过设置虚拟机参数来设置堆等区域的内存空间时,忽略直接内存大小就有可能致使各个内存区域总和大于物理内存限制,从而致使动态扩展时出现OutOfMemoryError异常

2. Java虚拟机对象

2.1 对象的建立

    1. 建立过程(通常对象,即不包括数组和Class对象):

(1)加载对象类:当虚拟机运行一条new指令时,首先检查这个指令的参数(也就是new后面的类名)是否能在常量池中定位到一个类的符号引用,而且检查这个类是否已被加载、解析和初始化过。若是没有,那就必须进行相应的类加载过程(该过程在后面会详细分析)。

(2)分配对象所需内存空间:

    在类加载完成后便可肯定对象所需的内存空间大小,给对象分配内存空间就是把一块肯定大小的内存从Java堆中划分出来。分配方式主要取决于虚拟机所采用的GC是否带有压缩整理功能。

    有压缩整理功能的GC会把Java堆分红两部分,一部分是被占用的,一部分是空闲的,经过一个指针做为分界的指示器,分配内存是只须要把指针向空闲区移动便可;不带压缩整理功能的GC会致使Java堆处于一种空闲与占用交错的内存空间,这时虚拟机就必须维护一个列表,记录堆中可用的内存空间,分配时只须要找到一个足够大小的空间划分给对象便可。

    可是,在并发状况下,以上两种方式也并不安全,好比,正在给对象A分配空间时,指针还未修改,对象B又占用了该指针来分配空间。因此,虚拟机采用了CAS加上失败重试的方式保证更新操做的原子性;另外一种方式是把内存分配动做按照线程划分在不一样的空间中进行,即每一个线程在堆中先分配一小块内存,称为本地线程分配缓冲(TLAB),那个线程要分配内存,就在那个线程的TLAB上分配,当使用完TLAB并分配新的TLAB时,才须要同步锁定,虚拟机使用TLAB可经过 -XX:+/-UseTLAB参数来设定。

(3)内存空间初始化:内存空间分配后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),若是使用TLAB,这一过程也会提早至TLAB分配时执行。该过程保证了对象即便不赋予初始值额可使用,能访问到的字段的数据类型均为所对应的零值。

(4)设置对象信息:例如这个对象是哪一个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分带年龄等信息。这些信息存放在对象的对象头中,根据虚拟机当前运行状态的不一样,是否启用偏向锁等。

(5)对象数据初始化:以上步骤完成后,对象已经建立成功,但此时对象内全部字段为零或null,此时便进行对象数据初始化,创造程序员所须要的对象。

2.2 对象的内存布局

虚拟机中,对象在内存中存储的布局能够分为3块区域:对象头,实例数据,对齐填充。

    1. 对象头:包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据被称为“Mark  Word”,这部分数据长度在32或64位的虚拟机中分别为32bit或64bit。对象须要存储的运行时数据不少,超出32或64bit所能记录的限度,但对象头信息是与对象自身定义的数据无关的额外存储成本,Mark  Word被设计为一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,例如在32位的HotSpot虚拟机中,若是对象处于未被锁定的状态下,则Mark  Word的32bit空间中,25bit用于存储对象哈希值,4bit用于存储对象分带年龄,2bit用于存储锁标志位,1bit固定为0,其余状态下的存储内容以下

存储内容 标志位 状态
对象哈希码、对象分代年龄、 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 重量级锁定
11 GC标记
偏向线程id、偏向时间戳、对象分代年龄 01 可偏向

    对象头的另外一部分数据是类型指针,即对象指向他的类元数据的指针,虚拟机经过该指针肯定对象所属的类,但并非全部的虚拟机实现都必须在对象数据上保留类型指针,也就是说查找对象的元数据信息并不必定要通过对象自己。对于数组对象,对象头中还必须友谊路爱用于记录数组长度的数据。

    2.实例数据:这里是对象真正存储的有效信息,包括各个字段的内容,不管是当前类的仍是父类的。

    3.对齐填充:这一部分并非必要存在的,也没有特殊含义,仅仅是为了使对象的大小必须是8字节的整数倍,若是对象的大小不足,则会进行填充补全。

2.3 对象的访问定位

建立对象是为了使用对象,在Java程序中,经过栈上的对象引用来操做堆上的对象,那么这个引用经过何种方式去定位和访问堆中的对象的具体位置?具体实现取决于虚拟机实现,主要有两种方法,使用句柄和直接指针。

    1.使用句柄访问:Java堆中划分一块内存做为句柄池,reference(引用)中存储的就是句柄池中存储的对象的句柄地址,而句柄包含了对象的实例数据与类型数据各自的具体地址信息。

    2.使用直接指针访问:reference(引用)中存储的就是对象地址

这两种访问方式各有优点,使用句柄访问的好处是引用中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的示例数据指针,而引用自己不须要修改。而使用直接指针访问方式的最大好处是速度更快,大部分虚拟机都会采用这种方式。

相关文章
相关标签/搜索