(本文内容主要为《深入理解Java虚拟机》学习笔记,部分内容摘自《深入理解Java虚拟机》)
Java虚拟机在执行Java程序时会把它管理的内存划分为若干个不同的数据区域。数据区域有各自用途、创建、销毁时间。有的区域随着虚拟机进程的启动而存在,有的则依赖用户线程的启动和结束而建立和销毁。

程序计数器

这是一块较小的内存空间,生命周期和线程相同。它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。Java虚拟机的多线程是通过流转切换并分配处理器执行时间的方式来实现的。同一时间,一个处理器(或多核处理器的一核)只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都有独立的程序计数器。

知识点

  • 线程私有,线程间的计数器互不影响,独立存储,生命周期和线程相同
  • 如果执行Java方法,其值为正在执行的虚拟机字节码指令的地址
  • 如果执行Native方法,其值为未定义(Undefined)
  • 此内存区域是JVM规范中唯一没有规定任何OutOfMemoryError情况的区域

(PS:原文中执行Native方法时,程序计数器值为空。oracle官方资料是这样的If the method currently being executed by the thread is native, the value of the Java Virtual Machine’s pc register is undefined. 知乎有大神解释此时值为未定义,即可以为任意值,但没有意义。一般认为,值为空即存储空间值为全0。根据官方资料,我更倾向于为任意值,但没有意义。)

问题

  1. 为什么是线程私有?

程序计数器的作用是记录线程下一条需要执行的字节码执行。每个线程只关心本身将要执行的字节码,并不关心其它线程将要执行的字节码。因此,程序计数器是私有的。

  1. 为什么执行Native方法时,值为空?

线程计数器是当前线程所执行的字节码的行号指示器。Native方法并没有被编译成字节码,所以无法记录。

  1. 为什么此区域没有OOME?

这是一个固定宽度的整数存储空间,因此不涉及OOME。来自知乎

  1. 从Native方法恢复时,程序计数器为空,那如何确认接下来要执行的代码?

这里的“pc寄存器”是在抽象的JVM层面上的概念——当执行Java方法时,这个抽象的“pc寄存器”存的是Java字节码的地址。实现上可能有两种形式,一种是相对该方法字节码开始处的偏移量,叫做bytecode index,简称bci;另一种是该Java字节码指令在内存里的地址,叫做bytecode pointer,简称bcp。对native方法而言,它的方法体并不是由Java字节码构成的,自然无法应用上述的“Java字节码地址”的概念。所以JVM规范规定,如果当前执行的方法是native的,那么pc寄存器的值未定义——是什么值都可以。上面是JVM规范所定义的抽象概念,那么实际实现呢?Java线程总是需要以某种形式映射到OS线程上。映射模型可以是1:1(原生线程模型)、n:1(绿色线程 / 用户态线程模型)、m:n(混合模型)。以HotSpot VM的实现为例,它目前在大多数平台上都使用1:1模型,也就是每个Java线程都直接映射到一个OS线程上执行。此时,native方法就由原生平台直接执行,并不需要理会抽象的JVM层面上的“pc寄存器”概念——原生的CPU上真正的PC寄存器是怎样就是怎样。就像一个用C或C++写的多线程程序,它在线程切换的时候是怎样的,Java的native方法也就是怎样的。来自知乎

虚拟机栈

这块区域也是线程私有,生命周期和线程相同。虚拟机栈描述了Java方法执行的模型:每个方法执行时都会创建一个栈帧(栈帧是方法运行时的基础数据结构)存储局部变量表、操作数栈、动态链表、方法出口等信息。每个方法从调用直至执行完成的过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。

知识点

  • 线程私有,生命周期和线程相同
  • 局部变量表存放了编译器可知的各种基本数据类型(boolean, byte, char, short, int, float, long, double, 对象引用和returnAddress类型)
  • 对象引用指reference类型,它不等同于对象本身,可能为一个指向对象起始地址的引用指针,也可能是一个指向代表对象的句柄或其他与此对象相关的位置
  • returnAddress类型指向一条字节码指令的地址
  • 64位长度的long和double会占据两个局部变量空间(slot),其他的数据类型只占据一个
  • 局部变量所需内存在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
  • Java虚拟机规范规定两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,扩展时如果无法申请到足够内存,会抛出OutOfMemoryError。

    问题

  1. 方法运行期间局部变量表大小不会改变,那操作数栈、动态链接、方法出口等信息占据的空间大小会改变吗?
  2. 操作数栈、动态链接、方法出口具体指的什么?
  3. 什么叫做线程请求的栈深度大于虚拟机所允许的深度?
  4. 虚拟机所允许的栈深度是怎么确定的?可以自定义吗?如果可以,怎么定义?
  5. long和double占据两个局部变量空间,那它们是大端还是小端?

本地方法栈

本地方法栈和虚拟机栈发挥的作用非常相似,区别是虚拟机栈视为虚拟机执行的Java方法(即字节码)服务,而本地方法栈则是为虚拟机使用的Native方法服务。

知识点

  • 线程私有,生命周期和线程相同
  • 会抛出StackOverflowError和OutOfMemoryError异常

    问题

  1. 本地方法栈会在什么情况下抛出StackOverflowError和OutOfMemoryError异常

对于大多数应用而言,堆是虚拟机锁管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术组件成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生。
从内存回收的角度看(现在收集器基本采用分代收集算法),堆中可以细分为新生代和老年代,再细分有Eden空间、From Survivor空间、To Survivor空间等;从内存分配角度看,堆中可能划分出线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。这些进一步划分都是为了更快的回收或分配内存。

知识点

  • 生命周期和虚拟机相同
  • 堆由所有线程共享
  • Java堆是垃圾收集器管理的主要区域
  • 根据Java虚拟机规范,堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像磁盘空间一样。大小可固定或可扩展。主流虚拟机中堆是可扩展的,如果堆中没有内存完成实例分配且堆也无法再扩展时,会跑出OutOfMemoryError

    问题

  1. 物理上不连续,逻辑上连续,具体是一种什么样的状态?
  2. 什么情况下,什么样的对象可以分配在堆之外?

    方法区

    方法去和堆一样,由各个线程共享,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开。
    运行时常量池是方法去的一部分。Class文件中有意向信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中。运行时常量池具备动态性。Java语言不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,例如String类的intern()方法。

    知识点

  • 生命周期和虚拟机相同
  • 内存区域可以不连续,大小可固定或可扩展,可以选择不实现垃圾收集
  • 当方法区(包括运行时常量池)无法满足内存分配需求时,会抛出OutOfMemoryError异常