JVM内存布局

时间:2023-03-09 03:49:18
JVM内存布局

1. 概述

  对于从事c和c++程序开发的开发人员来说,在内存管理领域,他们既拥有最高权力的”皇帝“又是从事最基础工作的”劳动人民“---既拥有每个对象的”所有权“,又担负着每个对象开始到终结的维护责任。java把内存控制的权利交给了java虚拟机,一旦出现内存泄露和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查将会是一项异常艰难的工作。

2. 运行时数据区域

  JVM内存布局

  JVM内存布局

  (1) 程序计数器

    程序计数器是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器不影响,独立存储,我们称这类内存区域为”线程私有“的内存。

    如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

  (2) java虚拟机栈

    与程序计数器一样,java虚拟机栈也是线程私有的,每个线程创建的同时都会创建VM栈,他的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧用于存储局部变量表(八种基本类型)、操作数帧、指向当前方法所属类的运行时常量池的引用、动态链接、返回结果等信息,非基本数据类型的对象在JVM栈上仅存放一个指向堆上的地址。

    当线程执行一个方法时,就会随之创建一个对应的栈帧,压入栈。方法执行完成后,就会将方法出栈。线程当前执行的方法所对应的栈帧位于VM栈的顶部。

    局部变量表存放了编译期可知的各种基本数据类型、对象引用。局部变量表所需的内存空间在编译期完全分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    对于这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

  (3) 本地方法栈

    虚拟机栈为虚拟机执行java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出*Error异常和OutOfMemoryError异常。

  (4) java堆

    java堆是java虚拟机所管理的内存中最大的一块,存储对象实例以及数组值的区域。java堆是被所有线程共享的一块内存区域,因此在其上进行对象内存的分配均需要进行加锁。java堆是垃圾收集器管理的主要区域。java堆细分为:新生代 和老年代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由Froom Space和To Space组成。java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  JVM内存布局

  特点:

    a. java堆是被所有线程共享的一块内存区域,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销,是比较大的。

    b. 新生代:新建的对象都是用新生代分配内存的。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivior区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且还存活的对象,将被复制到老年代。

    c. 老年代:在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。

    d. 永久代:实现方法区,主要存放所有已加载的类信息,方法信息,常量池等。永久代堆垃圾回收没有显著影响。

  (5) 方法区

·    方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、final类型的常量、静态变量、属性、方法即时编译器编译后的代码等数据。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

  (6) 运行时常量池

    运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池。当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。

3. 对象的创建

  虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。之后虚拟机将为新生对象分配内存。对象所需内存的大小在来加载完成后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。

  对象在内存存储的布局可以分为3块区域:对象头、实例数据和对齐填充。

    (1) 对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希吗、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    (2) 对象实例是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

    (3) 对齐填充:仅仅起到占位符的作用

  对象的访问定位:java程序需要通过栈上的referrnce数据来操作堆上的具体对象。主流的访问方式有使用句柄和直接指针两种。

    (1) 使用句柄访问的话,那么java堆中将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与数据类型各自的具体地址信息。

    (2) 使用直接指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型的相关信息,而reference中存储的直接就是对象的地址。