1. Java 内存区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域,不同区域负责不同功能。Java 8 和之前的版本略有不同,如下是 Java 8 之后的布局情况,移除了永久代,使用 Mataspace 代替。MetaSpace 并不在虚拟机中,而是使用本地内存。其中,程序计数器虚拟机栈本地方法栈是线程私有的,方法区是线程共享的。
image.png

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响。此区域是唯一 一个虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

虚拟机栈

每个虚拟机栈对应一个线程。虚拟机栈是线程私有的,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示 Java 方法执行的内存模型,每调用一个方法就会为每个方法生成一个栈帧,每个栈帧中都拥有:局部变量表(主要)、操作数栈、动态链接、方法出口信息。
JVM 基础 - 图2

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。堆中存储实例对象以及数组,数组也是保存在堆上的,因为在 Java 中,数组也是对象。

那么创建一个对象的时候,到底是在栈上分配还是在堆上分配呢?这就需要分类讨论了。

  • Java 对象的类型分为基本数据类型普通对象。对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个对象的引用保存在虚拟机栈的局部变量表中。
  • 对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。
    • 每个线程都拥有一个虚拟机栈。当我们在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。逃逸分析的情况下也可能会在栈上分配。
    • 其他情况,通常在堆上分配。

从 1.7 版本开始,字符串常量池就一直存在于堆上

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域。它不止是存『方法』,而是存储整个 class 文件的信息,JVM 运行时,类加载器子系统将会提取 class 文件里面的类信息,并将其存放在方法区中。例如类的名称、类的类型(枚举、类、接口)、字段、方法等。

2. 运行时常量池和字符串常量池随 JDK 版本的变化

  • 在JDK6及之前的版本中
    运行时常量池在方法区(永久代)中。
    字符串常量池在运行时常量池中。
  • 在JDK7版本中
    运行时常量池依然在方法区(永久代)中。在JDK7版本中,永久代的转移工作就已经开始了。
    字符串常量池被分配到了 Java 堆中。
  • 在JDK8版本中
    JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。
    字符串常量池仍存在于 Java 堆中。

    3. Java 对象的创建过程

    JVM 基础 - 图3
  1. 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程(加载、连接(验证、准备、解析)、初始化)。
  2. 分配内存:类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定。
  3. 初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 设置对象头: 初始化零值完成之后,虚拟机要对对象的对象头进行必要的设置
  5. 执行 init 方法: 执行 new 指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

    4. 对象的访问定位有哪两种方式?

  6. 句柄:如果使用句柄,那么虚拟机会在 Java 堆中划分一块内存出来作为句柄池。reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
    JVM 基础 - 图4

  7. 直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。
    JVM 基础 - 图5

这两种对象访问方式各有优势:

  • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
  • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

    5. JVM 具体是怎样运行 Java 字节码的?

  • 从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

  • 从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

    参考

  1. JVM中三个常量池(两种常量池)的解析及其随jdk版本的变化