Java JVM内存模型及存储案例解析

一、Java虚拟机模块划分

程序计数器、Java虚拟机栈(stack)、本地方法栈、Java堆(heap)、方法区、运行时常量池

1、程序计数器

 线程执行的字节码的行号指示器,字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。我们称这类内存区域为“线程私有”的内存。 唯一一个在Java虚拟机规范中没有规定任务OutOfMemoryError情况的区域。

2、Java虚拟机栈(stack)

java虚拟机栈也是线程私有的。

是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表存放了编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference:指向对象起始地址的引用指针、对象的句柄、对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据占用2个局部变量空间(Slot),其余占用1个。

局部变量表所需空间在编译期间完成分配,在方法运行期间不会改变大小。 两种异常:*Error与OutOfMemoryError异常

3、本地方法栈

Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机使用到Native方法服务。

4、Java堆(heap)

  • 内存中最大的一块
  • 线程共享的内存区域
  • 在虚拟机启动时创建
  • 唯一目的是存放对象实例
  • 垃圾收集器管理的主要区域

5、方法区

  • 线程共享的内存区域
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

6、运行时常量池

  • 方法区的一部分
  • 存储编译期生成的各种字面量和符号引用,在类加载后存放到运行是常量池中。

二、案例解析区域之间的关联关系

Object obj = new Object();  假设这句代码出现在方法体中

1、那么“Object obj”会反映到Java栈的本地变量表中,作为一个reference类型数据出现。

2、而“new Object()”会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存。

3、另外,在Java堆中还必须包含能查找到此对象类型数据(如何对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

4、由于reference类型在Java虚拟机规范里只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同新建实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

  • 句柄访问方式:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示:

                   Java JVM内存模型及存储案例解析

  • 直接指针访问方式:reference中直接存储的就是对象地址,如图所示:

                   Java JVM内存模型及存储案例解析

  • 句柄访问方式最大好处:在对象被移动(垃圾收集是移动对象是非常普遍的)时只会改变句柄中的实例数据指针,而reference本身不需要被修改;直接指针访问方式最大好处:速度更快,节省了一次指针定位的时间开销。