JVM数据分区

HotSpot虚拟机

HotSpot虚拟机是Oracle/OpenJDK中JVM的一种默认实现,其他实现还有BEA System公司的JRockit与IBM公司的IBM J9。

HotSpot虚拟机的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知即时编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准即时编译和栈上替换编译(On-Stack Replacement,OSR)行为

HotSpot虚拟机中含有两个即时编译器,分别是编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)以及编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)通常它们会在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统

自JDK 10起,HotSpot中又加入了一个全新的即时编译器:Graal编译器。Graal编译器是以C2编译器替代者的身份登场的。Graal能够做比C2更加复杂的优化,如“部分逃逸分析”(Partial Escape Analysis),也拥有比C2更容易使用激进预测性优化(Aggressive Speculative Optimization)的策略,支持自定义的预测性假设等。

然而需要预热才能达到最高性能等特点有悖于目前微服务架构的趋势。一种解决方案是提前编译,Substrate VM是在Graal VM 0.20版本里新出现的一个极小型的运行时环境,包括了独立的异常处理、同步调度、线程管理、内存管理(垃圾收集)和JNI访问等组件,目标是代替HotSpot用来支持提前编译后的程序执行。但要求目标程序是完全封闭的,即不能动态加载其他编译器不可知的代码和类库。

在JDK 9时期,HotSpot虚拟机开放了Java语言级别的编译器接口(Java Virtual Machine Compiler Interface,JVMCI),使得在Java虚拟机外部增加、替换即时编译器成为可能,Graal编译器就是通过这个接口植入到HotSpot之中。

到了JDK 10,HotSpot又重构了Java虚拟机的垃圾收集器接口(Java Virtual Machine Compiler Interface),统一了其内部各款垃圾收集器的公共行为。

Java技术体系

OpenJdk

  • Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK(Java Development Kit)
  • Java类库API中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment)

JVM从软件层面屏蔽不同操作系统在底层硬件与指令上的区别

JVM数据分区

  • 线程共享:方法区(元空间) 堆
  • 线程私有:虚拟机栈(线程栈) 本地方法栈 程序计数器

虚拟机栈

每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型

方法不能太长,一般不超过120行,过长会占用很多栈空间。拆分成模块,调用后马上弹出。不过也要考虑切换开销,这是时间换空间

这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。

在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

每条线程都需要有一个独立的程序计数器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。

此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

本地方法栈

本地方法以Native关键字修饰,没有具体实现,通过执行引擎让操作系统调用相应的dll文件,早期用于Java和C语言的交互,调用C语言实现的底层代码。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

方法区(元空间)

存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,这种特性被开发人员利用得比较多的便是String类的intern()方法。

在JDK 8以前,HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小)

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。

根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。


Java堆在虚拟机启动时创建。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,Java堆是垃圾收集器管理的内存区域

现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的

从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空间”“To Survivor空间”等名词,不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

直接内存

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

理解代码底层运行状态

反汇编 javap -c xx.class > xx.txt
根据JVM命令手册查询每行汇编代码的含义