Bootstrap

图文剖析JVM的运行时数据区

1.走近JVM

JVM是Java Virtual Machine(Java虚拟机)的缩写,运行Java字节码。符合Java语言规范的编程语言(比如Scala、Kotlin、Groovy,会编译成Java字节码),都可以运行在JVM上。

JVM是一个虚构出来的计算机,在实际的计算机上仿真模拟各种计算机功能。JVM有自己架构,处理器、堆栈等,还有相应的指令系统。

JVM本质上是一个应用程序,在不同的操作系统平台有不同的实现,但是都可以支持字节码文件(.class)运行。

Java所谓的“一次编译,多次运行”就是基于JVM实现的。跨平台也是这个意思,对于不同平台的差异,由JVM解决。这是一个很好的跨平台设计。

JVM还有一点,就是自动内存管理,这是不同于C++的重要的一点。

Java语言规范(The Java Language Specification)1,可以理解为一个模板蓝图,有很多条款。符合Java语言规范的编程语言,都可以编译生成字节码,运行在JVM上。

常见的:Scala、Kotlin、Groovy、JRuby、Jython/JPython。

你本事大的话,熟读Java语言规范,可以实现自己的语言。

Java虚拟机规范(Java Virtual Machine Specification)2,规定了实现JVM的框架。符合Java虚拟机规范的虚拟机,都可以运行字节码。常见的JVM有:HotSpot、IBM J9 VM、Graal VM等等。

你本事大的话,熟读JVM规范,可以实现自己的JVM。

2.走进JVM的运行时数据区3

Java程序员,借助JVM的自动内存管理机制,高效的开发应用程序,也不用过于担心内存泄漏和内存溢出的问题。如果不了解JVM如何使用内存,那排查问题寸步难行。

JVM规范,规定了运行时数据区域,有明确的划分。各自区域有自己的作用,互相协同,配合,为数以亿计的应用程序提供服务支持。

直接上图,清晰明了的展示。我们主要探讨的是运行时数据区域。

JVM结构

2.1.程序计数器

2.1.1.简述

程序计数器(Program Counter Register,官文为The Register pc)是一块比较小的内存空间,当前线程执行的字节码的行号指示器。每个线程都有自己的程序计数器(叫PC寄存器/寄存器也可以),字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令。

它是程序控制流的指示器,分支、循环、跳转、异常处 理、线程恢复等基础功能都需要依赖这个计数器来完成。

  1. 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
  2. 如果正在执行的是本地(Native)方法,这个计数器值则应为未定义(Undefined)。
2.1.2.概要总结
主要作用线程安全异常常见配置生命周期
记录当前线程执行的字节码的行号私有,线程安全没有任何异常同线程

2.2.Java虚拟机堆栈

2.2.1.简述

Java虚拟机堆栈(Java Virtual Machine Stacks,第一版JVM规范称其为Java堆栈,英文是Java Stacks)是Java方法执行的线程内存模型:每个方法被调用的时候,就会创建一个栈帧(Stack Frame)。

每个线程都有自己的堆栈,同线程一同创建,生命周期和线程的生命周期一致。

JVM堆栈不要求内存连续。只有出栈和入栈两个操作。

  1. 线程中每一次方法调用,就有一个对应的栈帧被压入JVM堆栈。
  2. 每一个方法调用结束(return或抛异常),就有一个栈帧被弹出Java堆栈。

栈帧存储了局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用直到执行完毕的过程,就对应一个栈帧在虚拟机堆栈入栈和出栈的过程。

Java虚拟机堆栈的数据结构大概如下:

栈:先进后出(FILO—First-In/Last-Out) ,有底有两壁没有盖子,像个水杯。

JVM-栈

JVM堆栈有两类异常:

  1. 如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;
  2. 如果Java虚拟机堆栈容量可以扩展,当扩展的时候无法申请到足够的内存会抛出OutOfMemoryError异常。
2.2.2.概要总结
主要作用线程安全异常常见配置生命周期
Java方法执行的线程内存模型私有,线程安全StackOverflowError
OutOfMemoryError
线程栈大小 -Xss同线程

2.3.本地方法栈

2.3.1.简述

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。

HotSpot将Java虚拟机堆栈和本地方法栈合二为一。

2.3.2.概要总结
主要作用线程安全异常常见配置生命周期
Java方法执行的线程内存模型私有,线程安全StackOverflowError
OutOfMemoryError
线程栈大小
-Xoss(HotSpot不起作用)
同线程

2.4.Java堆

2.4.1.简述

堆(Heap),是虚拟机所管理的内存中最大的一块,与Java应用程序关系最为密切,也是程序员需要重点关注的区域。堆是线程共享的。

堆是分配所有类实例(对象)和数组的内存运行时数据区域,几乎所有的对象都存放在堆中。
字符串常量池,也在堆中。(JDK8之前在方法区)

2.4.1.1.对象都在Java堆上分配吗?

这问题的上一句其实就是答案。前半句来自JVM官文,后半句来自一本书。书上的原话在下面

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

——《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明

官文的原文如下,几个LTS版本都行这句话。LTS不了解的话,可以参考我的博文JDK的两个大版本,为什么要选LTS_编程还未的博客-CSDN博客

The heap is the run-time data area from which memory for all class instances and arrays is allocated.

——JVM官文8,11,17

堆是为所有类实例和数组分配内存的运行时数据区域。

——Google翻译

官文是all class,书上是几乎。为什么会有一点差异呢。我的个人理解是这样的:

JVM官文规定的是JVM规范,没提到即时编译技术(JIT),没规定必须用。HotSpot作为基于JVM规范实现的虚拟机使用了JIT,以提高性能。JVM官文的大目录没看到即时编译相关的词语,我会继续读文档,如果有新发现会在这里修正。

——编程还未

所以,我的答案是分两种情况:

  1. JVM官方规范规定所有的对象和数组在堆分配。为了避免面试官把问题引导即时编译,就不要提了。
  2. 如果熟悉即时编译的话,那就是:虽然JVM官文规定所有对象和数组在堆分配,但是随着即时编译技术的提升……。然后要么被面试官问懵掉,要么面试官懵掉。

堆是在虚拟机启动时创建的。堆的内存由垃圾回收系统自动管理,也就是垃圾会搜器(Garbage Collector)。从垃圾回收内存的角度来看,现代垃圾回收器大部分基于分代收集理论设计。我们基于HotSpot来分析。

JVM-堆

上图就是HotSpot中常见的堆内存划分。

堆包含老年代和新生代,默认比例为2:1。新生代包含eden区(伊甸区)、from survivor区(s0)和to survivor区(s1),默认比例为8:1:1。

在绝大多数情况下,对象首先分配在eden区(伊甸园,出生地),在一次新生代回收后,还存活的对象会进入到s0区或s1区。s0和s1区也叫from区和to区,这是两块大小一样可以互换的内存空间。survivor是幸存的意思。

进到s0或s1之后,每经过一次新生代回收(Minor GC),对象如果存活,它的年龄会+1。当对象的年龄到达一定条件(默认15岁),就会进入老年代。老年代空间不足的时候会出发老年代回收(Major GC或叫做Full GC)。

堆在物理空间上不一定连续,在逻辑上是被要求连续的。所有线程共享堆,但是可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

Java堆可以被实现成固定大小,也可以扩展大小。目前主流的虚拟机都可以扩展大小,以HotSpot为例,配置参数为-Xms(初始值)-Xmx(最大值)。一般初始值和最大值设置为一样。因为堆内存占用从初始值涨到最大值,需要内存分配,耗费CPU资源。如果堆中没有足够的空间分配内存,Java虚拟机就会抛出OutOfMemmoryError异常。

2.4.2.概要总结
主要作用线程安全异常常见配置生命周期
存放对象实例和数组所有线程共享,线程不安全OutOfMemoryError1.新生代大小 -Xmn
2.堆初始值 -Xms
3.堆最大值 -Xmx
4.Eden区和from/to区
的比例 -XX:SurvivorRatio
同虚拟机

2.5.方法区

2.5.1.简述

方法区(Method Area)用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据(存储类的元数据)。方法区同Java堆一样,是所有线程共享的。

方法区中最重要的是类型信息、常量池、域信息、方法信息。

  1. 类型信息包括类的完整名称、父类的完整名称、类型修饰符(public、protected、private)和类型的直接接口列表。
  2. 常量池包括类方法、域等信息所引用的常量信息。
  3. 域信息包括域名称、域类型和域修饰符。
  4. 方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小及异常表。

方法区是在虚拟机启动时创建的。方法区在逻辑上的堆的一部分,但是可以选择不进行垃圾回收或压缩。

在HotSpot虚拟机中,实现方法区的是永久代(Permanent Generation)。这里的关系是这样的,JVM规范规定虚拟机要有方法区(Method Area),但是限制的内容不是很多。HotSpot团队把垃圾回收器的分代设计扩展到方法区,这样就可用像管理Java堆一样管理这部分内存。HotSpot用的实现就是永久代。其他虚拟机并没有永久代。实现方法区属于虚拟机实现细节,不受JVM规范管束,并不要求统一。

方法区并不是一个优秀的设计,所以Java8开始废弃了方法区,用元空间(Meta-Space)代替。方法区如果无法满足新的内存分配时,会抛出OutOfMemoryError人异常。

2.5.1.1.运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。每个类文件(Class文件)中,都定义了一组数据结构来表示类或接口运行时的表示形式,这就是运行时常量池。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

类或接口的运行时常量池是在 Java 虚拟机创建类或接口时构造的。如果没有足够的内存用来分配,则会抛出OutOfMemoryError异常。

每个Class文件中都有一个常量池表,在编译的时候生成,字节码里面可以看到。类或接口的二进制表示形式(字节码)中的常量池表用于在创建类或接口时构造运行时常量池。

这里的关系是这样的:常量池表(Constant Pool Table)在Class文件中,每个类都有一个;运行时常量池(Runtime Constant Pool)是方法区的一部分,JVM启动的时候在内存中逻辑划分;常量池表用于创建类或接口时构造运行时常量池。

运行时常量池中所有的引用最初都是符号引用。这些符号是按以下方式,从类或接口的二进制表示形式(字节码)中得到:以下CONSTANT的结构,都来自类或接口的的二进制表示(字节码)。

  • 类或接口的符号引用来自CONSTANT_Class_info结构。这种引用提供的类和接口名称,的格式和Class.getName()方法返回的一样 。
  • 类或接口的属性的符号引用来自CONSTANT_Fieldref_info结构。这种引用包含了属性的名称和描述符,以及指向属性所属类或接口的符号引用。
  • 类的方法符号引用来自CONSTANT_Methodref_info结构。这种引用包含了方法的名称和描述符,以及指向方法所属类的符号引用。
  • 接口的方法符号引用来自CONSTANT_InterfaceMethodref_info结构。这种引用包含了方法的名词和描述符,以及指向方法所属接口的符号引用。
  • 方法句柄的符号引用来自CONSTANT_MethodHandle_info结构。这种引用给出了方法描述符,具体取决于方法句柄的类型。
  • 方法类型的符号引用来自CONSTANT_MethodType_info。这种引用该处了方法描述。
  • 动态计算的常数符号引用来自CONSTANT_Dynamic_info。
  • 调用点限定符的符号引用来自CONSTANT_InvokeDynamic_info。
  • 字符串常量是指向String类实例的引用,来自CONSTANT_String_info结构。

Java语言规定,相同的字符串常量必须指向同一个String实例。**4

下面两项是正确的:

  1. 如果以前在包含与结构给定的相同的 Unicode 码位序列的类实例上调用了该方法,则字符串常量是同一类实例的一个。
  2. 否则,将创建一个新的类实例,其中包含结构给出的 Unicode 码位序列。字符串常量获取的结果是指向那个新String实例的引用。最后,新String实例的intern方法被虚拟机自动调用。

原文如下:
5.1. The Run-Time Constant Pool

  • A string constant is a reference to an instance of class String, and is derived from a CONSTANT_String_info structure. To derive a string constant, the Java Virtual Machine examines the sequence of code points given by the CONSTANT_String_info structure:
    • If the method String.intern has previously been invoked on an instance of class String containing a sequence of Unicode code points identical to that given by the CONSTANT_String_info structure, then the string constant is a reference to that same instance of class String.
    • Otherwise, a new instance of class String is created containing the sequence of Unicode code points given by the CONSTANT_String_info structure. The string constant is a reference to the new instance. Finally, the method String.intern is invoked on the new instance.

关于字符串的内容,可以参考我这篇博文String的特性详解-用源码、字节码和内存示意图详解,官文提供论点支撑_编程还未的博客-CSDN博客

2.5.2.概要总结
主要作用线程安全异常常见配置生命周期
存放类的结构
(类的元数据)
所有线程共享,线程不安全OutOfMemoryErrorHotSpot1.7及之前
1.永久代大小 -XX:PermSize
2.永久代上限 -XX:MaxPermSize
HotSpot1.8及之后
1.元空间大小-XX:MetaspaceSize
2.元空间上限-XX:MaxMetaspaceSize
同虚拟机

2.6.直接内存

直接内存(Direct Memory)并不是JVM运行时数据区的一部分。JVM规范中也没有规定直接内存。直接内存被频繁使用,也会发生OutOfMemoryError异常。

这部分资料比较少,直接引用一下

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

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务 器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常。

——《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明


  1. Java11语言规范 ↩︎

  2. Java11JVM规范 ↩︎

  3. 参考资料:Java11JVM规范、《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明、《实战JAVA虚拟机 JVM故障诊断与性能优化》葛一鸣、《深入理解 JVM&G1 GC》周明耀、《HotSpot实战》陈涛 ↩︎

  4. String Literals ↩︎

;