1.1 运行时数据区域
图片来自于深入理解JVM
1.1.1 程序计数器
程序计数器时一个较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
1.1.2 虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
1.1.3 本地方法栈
本地方法栈的作用与虚拟机栈相似,只是本地方法栈为虚拟机使用到的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法和数据结构没有强制的规定,因此具体的虚拟机可以自由地实现它。甚至有的虚拟机直接把本地方法栈和虚拟机栈合二为一。和虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError。
1.1.4 Java堆
对于大多数应用来说,Java堆是内存管理里最大的一块。它是所有线程共享的一块区域,在虚拟机启动时创建。它的唯一目的是在此分配对象实例,几乎所有的对象实例都在此进行分配。
1.1.5 方法区
也是所有线程共享的。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.1.6 运行时常量池
方法区的一部分。Class文件除了有类的版本、字段、方法、接口等描述信息之外,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
1.2 HotSpot虚拟机
1.2.1 对象的创建
虚拟机遇到一条new指令时,首先会去检查指令的参数是否会在常量池中定位到一个类的符号引用,并且检查整改符号引用代表的类是否已被加载、解析和初始化过,如果没有,那先执行类的加载过程。
在类加载检查通过后,接下来为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来。假设Java堆中的内存是绝对规整的,用过的内存放在一边,空闲的在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把那个指针向空闲区移动一段距离,这种分配方式称为指针碰撞。如果内存是不规整的,虚拟机必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为0值(不包括对象头)。接下来,虚拟机对对象进行必要设置,例如这个对象是哪个类的实例、如何找到类的元数据、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。
以上做好之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始——init方法还没有执行,所有字段都为0。所以,一般执行new之后会紧接执行init方法,把对象初始化,这样一个真正可用的对象才算完成产生出来。
1.2.2 对象的内存布局
在HotSpot虚拟机中,对象在内存中存储可分为3部分:对象头,实例数据和对齐填充。
1.2.3 对象的访问定位
建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现。目前访问方式有句柄和直接指针。