JVM之内存区域分配

   在上一篇文章中,我们讲了Java代码在JVM中的各种逻辑关系,那么本文就具体的讲述JVM各个组成部分。

JVM的内存区域划分

Java程序运行时数据区

  Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。JVM所管理的内存会包含以下几个内存区域:


程序计数器

    程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,比如分支、循环、跳转、异常处理、线程恢复等基础的功能都需要依赖这个计数器来完成。
    由于Java虚拟机的多线程是通过轮流切换处理器的执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只能执行一个线程中的指令,因此,为了使线程在切换后能够恢复到正确的位置,每一个程序计数器必须是独立的,也就是说程序计数器是每个线程所私有的,称之为此内存区域为线程私有的内存

虚拟机栈(线程栈)

    与程序计时器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同,虚拟机中描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈道出栈的过程。其中局部变量表中存放了编译器可知的各种基本数据类型、对象引用类型(不是对象本身)。
    在Java虚拟机规范中,对这个虚拟机栈区域规定了两种异常状况:
  (1)如果线程请求的栈深度大于虚拟机所允许的深度将会抛出StackOverflowError错误。
  (2)如果虚拟机栈可以动态拓展,如果拓展时为无法申请到足够的内存就会抛出OutMemoryError错误。

本地方法栈

     本地方法栈与虚拟机栈所发挥的作用是相似的,他们之间的区别只不过是虚拟机栈为虚拟机执行Java方法,而本地方法栈是执行虚拟机所使用到的Native方法服务,与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutMemoryError错误。

Java堆

    对于大多数应用该程序来说,Java堆屎Java虚拟机所管理的内存中最大的一块,Java堆屎被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目地就是存放对象实例,几乎所有的对象实例都在这里分配内存,JVM中规范说明了所有的对象实例以及数组都要在堆上分配。
    Java堆也是垃圾收集器所管理的主要区域,因此很多时候也被称为GC堆。Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。

方法区

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool)

    运行时常量池是方法区的一部分,Class文件中除了有泪的版本、字段、方法、借口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。Java虚拟机对Class文件的每一部分的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上都要求才会被虚拟机认可、加载和执行。
    运行时常量池对于Class文件常量池的另外一个特征就是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中的常量池的内容才能进入方法去运行时常量池,运行期间也可以将新的常量放入池中,这种特性体现最多的就是String的intern()方法。

对象的创建

    Java是一门面向对象的编程语言,在Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象(如克隆、反序列化)通常仅仅是new一个关键字而已,但是在虚拟机中,对象的创建确实很复杂的。
    当虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。内存分配完成以后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
    接下来虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据、对象的哈希码、对象的GC分代年龄等信息,这些信息都是包含对象的对象头重,根据虚拟机当前的运行状态的不同,如是否启用偏向锁等。
    以上所有工作完成后一个新的对象就已经产生了,但是从Java程序的角度来看,对象的创建才刚开始,因为其init方法还未执行,所有的字段都还是零,所以,一般来说,执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个对象才算是完全创建成功。
   小结一下:当类加载通过后,会按照以下的步骤来创建一个新的对象:
  (1)对象的数据进行初始化为零的操作(仅将对象的数据部分初始化,不包含对象头)。
  (2)对象头的设置。
  (3)对象数据的初始化,这个初始化是按照程序员给定的值进行初始化。

对象的内存布局

    在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充(这个只是启动占位符的作用)。

对象头

    HotSpot虚拟机的对象头包括两部分信息:
   (1)第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit.
   (2)另一部分就是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针。如果对象是一个Java数组,那在对象头重还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数据的元数据中却无法确定数组的大小。

实例数据

    实例数据部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略的参数和字段在Java源代码定义的顺序的影响。HotSpot虚拟机默认的分配策略是longs/doubles、ints、shorts/chars、bytes/booleans、oops,从分配策略可以看出,相同长度的字段总是被分配到一起,在满足这个条件的前提下,在父类中定义的变量会出现在子类之前,如果CompactFoelds参数值为true,那么子类中较窄的变量也可能会被插入到父类变量的空隙之中。

对象的访问定位

    建立对象是为了使用对象,我们的Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置,所以对象的访问取决于不同虚拟机的实现。
  (1)使用句柄访问
   如果使用句柄访问的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

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

   这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移到(垃圾收集时对象经常会被移动)只会改变句柄的实例数据指针,而reference本身并不需要修改。使用直接指针访问方式的最大好处就是速度快,它节省了一次指针定位的时间开销。

    总结

      本文主要介绍了JVM的内存划分情况以及各部分存储什么样的数据,以及对象在堆内存的分布情况、对象的构成,以及对象的访问和定位。我们需要重点注意的是Java对象的创建和初始化过程以及其在内存的分布情况,通过本文我们需要搞清楚一个问题:对象到底是什么?是的,对象的头部分主要是用于程序的各种逻辑实现,主要由JVM使用,我们平时使用到的对象就是指对象的实例数据部分,那么对象的实例数据部分到底有哪些数据?主要的,对象包含了Class的成员属性(全局变量)和类的行为(Method)等。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章