第二章 Java内存区域与内存溢出异常

来源:互联网 发布:京东和淘宝的竞品分析 编辑:程序博客网 时间:2024/06/09 00:40

2.2 运行时数据区域


图2-1 Java虚拟机运行时数据区

2.2.1 程序计数器

        程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
        由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器(对于多核心处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置上,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
        如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2.2.2 Java虚拟机栈

        Java虚拟机栈是线程私有的,生命周期与线程相同,虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
        局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。
        其中64位长度的long和double类型的数据会占用2个局部变量空间,其余的数据类型只占用1个,局部变量表所需的内存空间在编译期间完成分配,,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
        对于这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError异常。

2.2.3 本地方法栈

        本地方法栈与虚拟机栈所发挥的作用是非常的相似的,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

2.2.4 Java堆

        Java堆是被所有线程共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
        Java堆是垃圾收集器管理的主要区域,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

2.2.5 方法区

        方法区也是各线程共享的内存区域,它用于已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
        垃圾收集行为在这个区域是比较少出现的,这个区域的内存收集目标主要是针对常量池的回收和对类型的卸载。

2.2.6 运行时常量池

        运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用。

2.2.7 直接内存

        直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
`       在JDK1.4中新加入的NIO类,引入了基于通道与缓冲的I/O方式,它可以使用Native函数库直接分配堆外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native中来回复制数据。
        显然,本机直接内存的分配不会受到Java堆大小的限制,但肯定受到本机总内存大小限制。


2.3.1 对象的创建

        虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
        加载检查通过后,接下来将为新生对象分配内存。对象在所需内存的大小在类加载完成后便可以完全确定下来。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
        内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头之中。
        在完成以上工作后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始——<init>方法还没有执行,执行new指令之后会接着执行<init>方法,这样对象才算完全产生出来。
        大概的创建过程:遇到new指令,检查是否有一个类的符号引用,并且检查符号引用代表的类是否已被加载、解析和初始化过——>没有,执行类加载——>分配内存——>内存空间初始化为零值——>设置对象的对象头——>执行<init>初始化方法,完成对象的创建。

2.3.2 对象的内存布局

        在HotSpot虚拟机中,对象在内存中的存储的布局可以分为3块区域:对象头、实例数据和对象填充。
        对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等,另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。如果对象是数组,还必须有一块用于记录数组长度的数据。
        实例数据部分是对象真正存储有效信息,在程序代码中所定义的各种类型的字段内容,无论是是从父类继承下来的,还是在子类中定义的,都要记录下来。
        对象填充部分并不是必然存在的,它仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,当对象实例数据没有对齐时,就需要通过对齐填充来补全。

2.3.3 对象的访问定位

        建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问有使用句柄和直接指针两种。
        使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据与类型数据各自的具体地址信息,相当于间接访问了对象。

        使用直接指针访问,reference中存储的直接就是对象地址,直接访问对象。

        两种对象访问方式各有优势,HotSpot使用的是第二种方式进行对象访问。

阅读全文
0 0