jvm-元空间

基础概念:

  1. 方法区:jvm规范中的定义,指一片内存区域,用于存放加载到内存中的类信息、常量池等。

  2. 永久代:JDK1.7(含)之前方法区的实现方式,使用永久代实现主要是为了把GC分代收集扩展至方法区,省去了专门为方法区编写内存管理代码的工作。

  3. 元空间:JDK1.8(含)之后的方法区实现。

  4. instanceKlass :java类的运行时结构数据,就是常说的类的元数据,由于jvm底层C++实现,java应用程序不能直接访问该对象,而是通过java.lang.Class类的实例间接访问该部分信息。xx.class对象是java程序访问xx类instanceKlass 数据的接口,且xx.class对象其实是存在堆里的。

  5. 指针压缩

    • 64位平台上默认打开
    • 设置-XX:+UseCompressedOops压缩对象指针, oops指的是普通对象指针(ordinary object pointers), 会被压缩成32位。
    • 设置-XX:+UseCompressedClassPointers压缩类指针,会被压缩成32位。
  6. 类指针压缩空间(Compressed Class Pointer Space):对于64位平台,为了压缩JVM对象中的_klass指针的大小,引入了类指针压缩空间。

    • 只有是64位平台上启用了类指针压缩才会存在这个区域。
    • 类指针压缩空间会有一个基地址

    一个64位的地址,只取后32位,那么前32位固定不变的前提下,地址空间的大小是2的32次方

1. 永久代被取代

Permanent Generation space是指内存的永久保存区域,用于存放Class和Meta的信息,类在被加载的时候被放入PermGen space区域,它和存放对象的堆区域不同,所以应用程序会加载很多类的话,就很可能出现永久代溢出错误,这种错误常见在web服务器对jsp进行预编译的时候。

1.1 为什么移除持久代

  • 永久代空间大小是在启动时固定好的——运行时很难进行调优。-XX:MaxPermSize,设置成多少好呢?
  • HotSpot的内部类型也是Java对象:它可能会在Full GC中被移动,同时它对应用不透明,且是非强类型的,难以跟踪调试,还需要存储元数据的元数据信息(meta-metadata)。
  • 简化Full GC:每一个回收器有专门的元数据迭代器。
  • 可以在GC不进行暂停的情况下并发地释放类数据。
  • 使得原来受限于持久代的一些改进未来有可能实现

根据上面的各种原因,永久代最终被移除,方法区移至Metaspace,字符串常量移至Java Heap

1.2 移除持久代后,PermGen空间的状况

  • 这部分内存空间将全部移除。
  • JVM的参数:-XX:PermSize 和-XX:MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。

2. 元空间

随着JDK1.8的到来,JVM不再有PermGen。但类的元数据信息还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。

2.1 Metaspace的组成

  • Klass Metaspace
  • 这块内存最多只会存在一块,用来存 instanceKlass

  • 这部分默认放在类指针压缩空间中,是一块连续的内存区域,和之前的perm一样紧接着Heap。通过-XX:CompressedClassSpaceSize来控制这块内存的大小,默认是1 G。

  • 但是这块内存不是必须的,如果设置了-XX:-UseCompressedClassPointers,或者-Xmx设置大于32 G,就不会有这块内存,这种情况下instanceKlass都会存在NoKlass Metaspace里。
  • NoKlass Metaspace:

    • 用来存instanceKlass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。

    • 这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存instanceKlass的内容,上面已经提到了对应场景。

    • NoKlass Metaspace在本地内存中分配。

Klass Metaspace和NoKlass Metaspace 都是所有class-loader共享的,所以类加载器们要分配内存,但是每个类加载器都有一个SpaceManager,来管理属于这个类加载的内存小块。如果Klass Metaspace用完了,那就会报OOM异常,不过一般情况下不会,NoKlass Metaspace是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。

2.2 Metaspace的几个参数

如果我们要改变Metaspace的一些行为,我们一般会对其相关的一些参数做调整,因为Metaspace的参数本身不是很多,所以我这里将涉及到的所有参数都做一个介绍。

  • MetaspaceSize :初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

  • MaxMetaspaceSize :最大空间,默认是没有限制的。

  • MinMetaspaceFreeRatio :在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

  • MaxMetaspaceFreeRatio :在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

  • CompressedClassSpaceSize :默认1 G,这个参数主要是设置Klass Metaspace的大小,不过这个参数设置了也不一定起作用,前提是能开启压缩指针,假如-Xmx超过了32 G,压缩指针是开启不来的。如果有Klass Metaspace,那这块内存是和Heap连着的。

  • MinMetaspaceExpansion :MinMetaspaceExpansion和MaxMetaspaceExpansion这两个参数或许和大家认识的并不一样,也许很多人会认为这两个参数不就是内存不够的时候,然后扩容的最小大小吗?其实不然

    这两个参数和扩容其实并没有直接的关系,也就是并不是为了增大committed的内存,而是为了增大触发metaspace GC的阈值

    这两个参数主要是在比较特殊的场景下救急使用,比如gcLocker或者should_concurrent_collect的一些场景,因为这些场景下接下来会做一次GC,相信在接下来的GC中可能会释放一些metaspace的内存,于是先临时扩大下metaspace触发GC的阈值,而有些内存分配失败其实正好是因为这个阈值触顶导致的,于是可以通过增大阈值暂时绕过去

    默认332.8K,增大触发metaspace GC阈值的最小要求。假如我们要救急分配的内存很小,没有达到MinMetaspaceExpansion,但是我们会将这次触发metaspace GC的阈值提升MinMetaspaceExpansion,之所以要大于这次要分配的内存大小主要是为了防止别的线程也有类似的请求而频繁触发相关的操作,不过如果要分配的内存超过了MaxMetaspaceExpansion,那MinMetaspaceExpansion将会是要分配的内存大小基础上的一个增量

  • MaxMetaspaceExpansion :默认5.2M,增大触发metaspace GC阈值的最大要求。假如说我们要分配的内存超过了MinMetaspaceExpansion但是低于MaxMetaspaceExpansion,那增量是MaxMetaspaceExpansion,如果超过了MaxMetaspaceExpansion,那增量是MinMetaspaceExpansion加上要分配的内存大小

    注:每次分配只会给对应的线程一次扩展触发metaspace GC阈值的机会,如果扩展了,但是还不能分配,那就只能等着做GC了

  • UseLargePagesInMetaspace :默认false,这个参数是说是否在metaspace里使用LargePage,一般情况下我们使用4 KB的page size,这个参数依赖于UseLargePages这个参数开启,不过这个参数我们一般不开。

  • InitialBootClassLoaderMetaspaceSize :64位下默认4M,32位下默认2200K,metasapce前面已经提到主要分了两大块,Klass Metaspace以及NoKlass Metaspace,而NoKlass Metaspace是由一块块内存组合起来的,这个参数决定了NoKlass Metaspace的第一个内存Block的大小,即2*InitialBootClassLoaderMetaspaceSize,同时为bootstrapClassLoader的第一块内存chunk分配了InitialBootClassLoaderMetaspaceSize的大小

2.3 Metaspace内存管理

  1. 在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。
  2. 每个加载器有单独的存储空间。
  3. 省掉了GC扫描及压缩的时间。
  4. 当GC发现某个类加载器不再存活了,会把对应的空间整个回收。

参考文档:

Metaspace 之一:Metaspace整体介绍(永久代被替换原因、元空间特点、元空间内存查看分析方法

JVM源码分析之Metaspace解密

JDK8 的FullGC 之 metaspace

JVM学习——元空间(Metaspace)