补充问题: 1.7 1.8 11 13 16 这些版本的JDK 的内存区域是否也是这样划分?


什么是运行时数据区?


在 JVM 执行 Java 程序的时候,会将 JVM 所管理的内存切分成若干个区域。 这些被 JVM 管理的内存区域就是运行时数据区域(Runtime Data Area)。

运行时数据区域是怎么划分的?

主要分为两大部分:
1) 每个线程分配的内存空间,线程独有,其他线程不能访问,生命周期跟随线程的生命周期。
2) 各个线程共享的内存空间,各个线程可以共同访问,生命周期跟随虚拟机生命周期。

运行时数据区域的划分可以用下图表示:
😄JVM运行时数据区介绍 - 图1

每个部分有什么作用?


我们编写的 Java 类源文件,会被编译成 .class 文件,然后加载入内存执行。那么存放 class 文件的空间成为方法区

方法区

  • 也叫做非堆(Non-heap),被各个线程共享
  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 不需要连续的物理内存,可以选择固定大小或可扩展
  • 可以选择不实现垃圾收集
  • 相对较少执行垃圾回收,主要是针对常量池的回收和类型的卸载,回收条件较为苛刻
  • 无法满足内存分配需求时,将抛出OutOfMemeryError异常

一般来说,程序从 main() 方法开始执行。所以需要一个东西,来标记当前字节码指令执行到哪个位置,哪行代码,这个东西就成为程序计数器。由于 Java 是多线程的,而且每个线程都需要标记执行到哪里,所以每个线程会有自己的程序计数器来标记当前线程的执行位置。

程序计数器

这个是一个比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。Java多线程分配策略是时间片轮转的策略,所以每个线程都需要建立独立的程序计数器,各个线程之间的计数器互相独立,才能保证,线程恢复执行的时候的字节码行号的准确性。

  • 线程独立,生命周期与线程相同,每个线程有自己独立的程序计数器。
  • 如果线程正在执行Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。
  • 如果线程正在执行Native方法,计数器则为空(Undefined)。
  • 唯一一个虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

代码的执行,也就是一个个方法的执行,一般来说,方法里面会有很多本地变量。存储这些方法的变量的区域成为 Java 虚拟机栈 。因为多个线程可能执行的方法不一样,所以,每个线程都有自己的栈空间。更详细的划分是,每个方法调用的时候会创建一个叫做栈桢的数据结构,然后压入栈空间,方法的变量等数据就是存放在栈桢里面。方法调用完毕后,栈桢就会出栈。

Java 虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型。每个方法执行的同时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的整个过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 线程私有的,生命周期与线程相同。
  • 局部变量表存放了编译器可知的各种基本数据类型,对象引用和returnAddress类型(指向了一条字节码指令的地址)。
  • 线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
  • 如果虚拟机可以动态扩展,当扩展的时候无法申请足够的内存,将抛出OutOfMemeryError异常。

和Java虚拟机栈很相似,不同的点是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。

以上所讲到的区域都是线程独立的,每个空间互不影响。

实际上,在方法里面创建对象,例如 Object o = new Object() 。 那么变量 o 是会存在在栈桢的局部变量表里面,但是,这里只是一个引用,指向了 new Object() 创建的 Object 实例。这个 Object 实例是存放在一个称为的区域里面。

堆(heap)空间

  • 堆是虚拟机管理的内存中最大的一块。
  • 被所有线程共享,在虚拟机启动的时候创建。
  • 唯一的目的就是存放对象实例,几乎所有的实例对象和数组都要在堆上分配,但不绝对。
  • 垃圾收集器管理的主要区域。
  • 可以处于物理上不连续的内存空间中。
  • 堆中没有内存完成实例分配,并且堆无法再扩展时,将会抛出OutOfMemeryError异常。

在垃圾回收概念里面,堆空间就会有新生代和老年代,还有永久代。注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代也将被移除,改用 metaspace 实现。

这样的划分只是为了垃圾回收的时候更加高效,这样的分代是垃圾回收器的概念,CMS ,ParNew,G1 就会有分代的概念,在 ZGC 里面,暂时不进行分代。 但 G1 的分代概念是逻辑上的概念,不是物理上的概念。
😄JVM运行时数据区介绍 - 图2

运行时常量池

  • 在JDK1.7中,常量池是在方法区中,JDK1.8就放到了堆里面了。
  • Class文件中,有一项信息叫做常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
  • 运行时也可以将新的常量放入池中
  • 常量池无法申请到内存时,将会抛出OutOfMemeryError异常

参考:
[1]《深入理解Java虚拟机》第二版
[2] JVM 学习 ——Native 方法