从Java虚拟机规范看HotSpot虚拟机的内存结构和变迁

引言

网上有大量讨论JVM的内存模型的文章,但很多内容都是到处摘抄而来,导致许多概念模糊不清。

比如对于“JVM内存模型”和“Java内存模型(JMM)”没有区分,实际上,Java内存模型(JMM)是一种规范,和具体的Java虚拟机的内存结构不是一个概念,不应该把诸如“年轻代“、”老年代”这类关于虚拟机具体实现的讨论归为Java内存模型。

而在具体讨论JVM的内存结构时,还应该指出,我们通常讨论的都是HotSpot虚拟机中的实现,这些模型并不是所有虚拟机通用的,比如“Perm Gen(永久代)”就是HotSpot中的概念,JRockit中并没有永久代。

此外,不应该把“永久代”和“方法区”混为一谈,永久代(Perm Gen)只是HotSpot对于Java虚拟机规范中方法区(Method Area)的一种实现,后来被改成了元空间(MetaSpace),文中会具体介绍这些变化。

本文希望从Java虚拟机规范出发,尽可能通过查阅官方文档,以及阅读HotSpot VM中的部分核心源代码的方式,重新梳理Java虚拟机的内存结构,重点讨论:

  • HotSpot虚拟机中,Heap(堆),Method Area(方法区)和Run-Time Constant Pool(运行时常量池)的关系
  • Method Area的在JDK1.6,JDK1.7和JDK1.8中的变迁(Perm Gen的消失和MetaSpace的出现)
  • 字符串常量池的转移以及运行时常量池和intern方法的变化等。

而Jvm中的The pc RegisterJava Virtual Machine StacksNative Method Stacks这些部分,则不在本文的讨论范围之内。

Java虚拟机规范中的内存模型

Java虚拟机规范上指定了Java虚拟机的运行时数据区包括The pc Register、Java Virtual Machine Stacks、 Heap、 Method Area、Run-Time Constant Pool和Native Method Stacks这些部分,其中PC寄存器,Java虚拟机栈和本地方法栈会为每个线程所创建,属于线程私有,而堆,方法区和运行时常量池是所有线程共享的。

PC寄存器,Java虚拟机栈和本地方法栈的作用与传统的操作系统类似,这里不多赘述,我们主要关注Heap(堆),Method Area(方法区)和Run-Time Constant Pool(运行时常量池)的规范。

Heap(堆)

首先查看Java虚拟机中对Heap的定义:

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

上面的定义指出,Heap是Java虚拟机中为所有Java虚拟机线程所共享的内存区域,它是一块为所有对象和数组分配内存的运行时数据区。

规范中还有下面的一段描述:

The heap is created on virtual machine start-up. Heap storage for objects is reclaimed by an automatic storage management system (known as a garbage collector); objects are never explicitly deallocated. The Java Virtual Machine assumes no particular type of automatic storage management system, and the storage management technique may be chosen according to the implementor's system requirements. The heap may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger heap becomes unnecessary. The memory for the heap does not need to be contiguous.

文字有点长,不过我们可以总结出几个有关Heap的要点:

  • Heap是在Java虚拟机启动时创建的
  • Heap中的对象占用的空间由自动存储管理系统所回收(其实就是GC),对象不能被显式回收
  • 自动存储管理系统(垃圾收集器)没有统一的实现,由虚拟机的实现者来选择
  • Heap的空间大小可以是固定的,也可以进行扩充和收缩,Heap不需要连续的内存空间

看完Java虚拟机规范中对Heap的描述,我们最需要记住的一点是:Heap是一块为所有对象和数组分配内存的运行时数据区

Method Area(方法区)

首先看Java虚拟机规范中对Method Area的定义:

The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization

上面一段文字的含义是,Method Area(方法区)是Java虚拟机中为所有Java虚拟机线程所共享的内存区域,它类似于传统语言中存储编译后代码的区域,或者可以说它类似于操作系统进程中的'text'段(代码段,在操作系统中内存会分为数据段,代码段,堆,栈和BBS段)。

Method Area用于保存每个类的结构信息,如运行时常量池、字段和方法数据、以及方法和构造器的代码,包括用于类,对象和接口初始化的特殊方法

下面还有一段描述,看起来跟对Heap的描述很像:

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.

上面的文字有以下要点:

  • Method Area是在Java虚拟机启动时创建的
  • Method Area在逻辑上是Heap的一部分,但可以选择不对它进行垃圾收集
  • Java虚拟机规范不强制规定Method Area的存储位置和管理已编译代码的策略
  • Heap的空间大小可以是固定的,也可以进行扩充和收缩,Heap不需要连续的内存空间

从Java虚拟机规范的描述可以看出,规范对Method Area的定义是比较宽泛,只是定义了一块内存区域,用于存储类的结构信息。它没有严格定义Method Area在内存中的位置,也没有规定对它进行垃圾回收等管理策略。

因此,我们不应该认为Method Area和Heap是完全割裂的两块内存区域,它甚至可以是Heap的一部分

Run-Time Constant Pool(运行时常量池)

老样子,先看定义:

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4). It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time. The run-time constant pool serves a function similar to that of a symbol table for a conventional programming language, although it contains a wider range of data than a typical symbol table.

上面的第一句很重要:运行时常量池是每个类/接口的字节码文件中constant_pool table的运行时实现。意思就是,每个类/接口都会拥有一个和字节码中的常量池对应的运行时常量池

它包含了各种常量,包括编译时已知的数值字面量、运行时解析的方法和字段引用。

Run-Time Constant Pool的功能和传统编程语言的符号表类似,不过它包含的符号类型更广。

第二段照旧有一段描述:

Each run-time constant pool is allocated from the Java Virtual Machine's method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine.

这里有个重要信息:每个运行时常量池都是从Java虚拟机的Method Area中分配的,它和class/interface一起被Java虚拟机所创建。

这说明了Run-Time Constant Pool是Method Area的一部分,这和Method Area中的描述是相符的。

小结HotSpot VM的内存结构

简单总结一下上面虚拟机规范的内容:

  • Heap用于为所有对象和数组分配内存
  • Method Area用于保存类/接口的结构信息
  • Run-Time Constant Pool用于保存各种常量

我画了一张简单的示意图来展示它们之间的关系:

image-20201213235106728

这个图中,我把Heap和Method Area分成了两个互相隔离的区域,Java虚拟机规范并没有要求这么做,不过HotSpot虚拟机的早期实现和图中是类似的。

Method Area和Heap分开也是比较合理的,因为两者保存的数据类型不一样,数据的生命周期也不相同,分开存储更有利于管理和回收。

HotSpot VM的内存模型变迁

在这一部分,我将通过三张内存结构图来描述HotSpot虚拟机的内存模型在JDK1.6,JDK1.7和JDK1.8中的变迁,请忽略各个区域的大小比例,重点关注各个区域的转移。

JDK1.6 Perm Gen作为Method Area的实现

在JDK1.6中,永久代(Perm Gen)作为Method Area的实现,这里保存着类的静态变量(Class statics),字符串常量池(String Table),符号引用(Symbols)和字面量(Interned Strings)。

这个时期的永久代和堆是相邻的,使用连续的物理内存,但是内存空间是隔离的。

永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。

永久代的内存受到Java虚拟机的管理。

image-20201213235123402

JDK1.7 数据向Java Heap和Native Heap迁移

在JDK1.7中,Perm Gen的数据开始向Java Heap和Native Heap转移:

  • 字符串常量池(String Table)转移到了Java Heap
  • 字面量(Interned Strings)转移到了Java Heap
  • 类的静态变量(Class Statics)转移到了Java Heap
  • 符号引用(Symbols)转移到了Native Heap

Java Heap和Native Heap有什么区别?

Native Heap是操作系统层面的堆区,是JVM进程运行时动态向操作系统申请的内存空间。JVM会在Native Heap中划出一块区域作为Java Heap(也有JVM Heap的说法,本文使用Oracle官网的名词)。Java Heap就是Java虚拟机规范里面的Heap。

具体可以参考这篇回答:native memory和native heap及GC heap有什么关系?

为什么要转移永久代的数据?

因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,容易遇到OOM,比如使用动态代理时,需要大量加载类文件,这时候很容易就发生java.lang.OutOfMemoryError: PermGen Space的异常。

为了减少永久代的压力,因此JDK1.7开始把数据向堆和本地内存迁移。

image-20201213235142865

JDK1.8 MetaSpace成为Method Area的实现

到了JDK1.8,HotSpot直接使用MetaSpace取代了Perm Gen。

自此,HotSpot虚拟机中不再有Perm Gen(永久代),只有MetaSpace(元空间)。

下面直接贴一段Oracle的官方资料中对MetaSpace的描述:

  • JDK 8 does not have Permanent Generation
  • Class metadata is stored in a new space called Metaspace
  • Not contiguous with the Java Heap
  • Metaspace is allocated out of native memory
  • Maximum space available to the Metaspace is the available system memory
  • This can though be limited by MaxMetaspaceSize JVM option

可以看到,元空间对比老年代有很多优点,它不再和Java Heap使用相邻的物理内存,直接从本地内存分配空间,元空间大小的上限受限于系统的内存大小,因此发生OOM的概率可以大大降低。当然,我们还是可以使用MaxMetaspaceSize选项来限制MetaSpace的大小。

image-20201213235151420

字符串常量池和intern()方法

在HotSpot内存模型的变迁过程中,还有一个地方值得特别关心,那就是String Table(字符串常量池)。

String Table在JDK1.6中位于Perm Gen,但是在JDK1.7中被转移到了Java Heap中,这次转移伴随着String.intern()方法的性质发生了一些微小的改变。

  • 在1.6中,intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该对象的引用。如果没有找到,则将该字符串常量加入到字符串常量区,也就是在永久代中创建该字符串对象,再把引用保存到字符串常量池中。
  • 在1.7中,intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该对象的引用,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中。

这里只是简单提一下结论,具体的细节会写一篇文章来介绍一下,敬请期待。

结语

至此,本文对Java虚拟机规范中关于JVM内存区域的描述做了简单的解读,并以HotSpot虚拟机为例说明了具体实现和规范之间的联系。

Java虚拟机规范是一份与实现无关的文档,它在描述时没有规定具体的实现细节,显得"模棱两可",但所有的Java虚拟机实现都应该遵循这个规范。

其中还有关于类文件格式,字节码指令等相关内容的描述,感兴趣的读者可以自行前往阅读。

另外,Oracle官网的教程PPT也是一份不错的资料:HotSpot JVM Memory Management

关于字符串常量池的细节,放在下一篇文章来讨论。

参考资料

posted @ 2021-03-28 20:02  Yuchao_Huang  阅读(861)  评论(0编辑  收藏  举报