JVM元空间替换持久代

概述

metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所致,看到大家讨论来讨论去,看得出很多人对metaspace还是模棱两可,不是很了解它,因此我觉得有必要写篇文章来介绍一下它,解开它神秘的面纱,当我们再次碰到它的相关问题的时候不会再感到束手无策。

为什么会有metaspace

metaspace的由来民间已有很多传说,不过我这里只谈我自己的理解,因为我不是oracle参与这块的开发者,所以对其真正的由来不怎么了解。

我们都知道jdk8之前有perm这一整块内存来存klass等信息,我们的参数里也必不可少地会配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,jvm在启动的时候会根据这些配置来分配一块连续的内存块,但是随着动态类加载的情况越来越多,这块内存我们变得不太可控,到底设置多大合适是每个开发者要考虑的问题,如果设置太小了,系统运行过程中就容易出现内存溢出,设置大了又总感觉浪费,尽管不会实质分配这么大的物理内存。基于这么一个可能的原因,于是metaspace出现了,希望内存的管理不再受到限制,也不要怎么关注元数据这块的OOM问题,虽然到目前来看,也并没有完美地解决这个问题。

或许从JVM代码里也能看出一些端倪来,比如MaxMetaspaceSize默认值很大,CompressedClassSpaceSize默认也有1G,从这些参数我们能猜到metaspace的作者不希望出现它相关的OOM问题。

元空间替换持久代

1.1、持久代

  PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,说说为什么会内存益出:这一部分用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和和存放Instance的Heap区域不同,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。这种错误常见在web服务器对JSP进行pre compile的时候。

  JVM 种类有很多,比如 Oralce-Sun Hotspot, Oralce JRockit, IBM J9, Taobao JVM(淘宝好样的!)等等。当然武林盟主是Hotspot了,这个毫无争议。需要注意的是,PermGen space是Oracle-Sun Hotspot才有,JRockit以及J9是没有这个区域。

持久代中包含了虚拟机中所有可通过反射获取到的数据,比如Class和Method对象。不同的Java虚拟机之间可能会进行类共享,因此持久代又分为只读区和读写区。

JVM用于描述应用程序中用到的类和方法的元数据也存储在持久代中。JVM运行时会用到多少持久代的空间取决于应用程序用到了多少类。除此之外,Java SE库中的类和方法也都存储在这里。

如果JVM发现有的类已经不再需要了,它会去回收(卸载)这些类,将它们的空间释放出来给其它类使用。Full GC会进行持久代的回收。

  • JVM中类的元数据在Java堆中的存储区域。
  • Java类对应的HotSpot虚拟机中的内部表示也存储在这里。
  • 类的层级信息,字段,名字。
  • 方法的编译信息及字节码。
  • 变量
  • 常量池和符号解析

持久代的大小

  • 它的上限是MaxPermSize,默认是64M
  • Java堆中的连续区域 : 如果存储在非连续的堆空间中的话,要定位出持久代到新对象的引用非常复杂并且耗时。卡表(card table),是一种记忆集(Remembered Set),它用来记录某个内存代中普通对象指针(oops)的修改。
  • 持久代用完后,会抛出OutOfMemoryError "PermGen space"异常。解决方案:应用程序清理引用来触发类卸载;增加MaxPermSize的大小。
  • 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小。

1.2、为什么移除持久代

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

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

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

  • 这部分内存空间将全部移除。

  • JVM的参数:PermSize 和 MaxPermSize 会被忽略并给出警告(如果在启用时设置了这两个参数)。 

metaspace的组成

metaspace其实由两大部分组成

  • Klass Metaspace
  • NoKlass Metaspace

Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。这块内存是紧接着Heap的,和我们之前的perm一样,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。

NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容,上面已经提到了对应场景。

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

metaspace的几个参数

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

  • UseLargePagesInMetaspace
  • InitialBootClassLoaderMetaspaceSize
  • MetaspaceSize
  • MaxMetaspaceSize
  • CompressedClassSpaceSize
  • MinMetaspaceExpansion
  • MaxMetaspaceExpansion
  • MinMetaspaceFreeRatio

UseLargePagesInMetaspace

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

InitialBootClassLoaderMetaspaceSize

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

MetaspaceSize

默认20.8M左右(x86下开启c2模式),主要是控制metaspaceGC发生的初始阈值,也是最小阈值,但是触发metaspaceGC的阈值是不断变化的,与之对比的主要是指Klass Metaspace与NoKlass Metaspace两块committed的内存和。

MaxMetaspaceSize

默认基本是无穷大,但是我还是建议大家设置这个参数,因为很可能会因为没有限制而导致metaspace被无止境使用(一般是内存泄漏)而被OS Kill。这个参数会限制metaspace(包括了Klass Metaspace以及NoKlass Metaspace)被committed的内存大小,会保证committed的内存不会超过这个值,一旦超过就会触发GC,这里要注意和MaxPermSize的区别,MaxMetaspaceSize并不会在jvm启动的时候分配一块这么大的内存出来,而MaxPermSize是会分配一块这么大的内存的。

CompressedClassSpaceSize

默认1G,这个参数主要是设置Klass Metaspace的大小,不过这个参数设置了也不一定起作用,前提是能开启压缩指针,假如-Xmx超过了32G,压缩指针是开启不来的。如果有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了

MinMetaspaceFreeRatio

MinMetaspaceFreeRatio和下面的MaxMetaspaceFreeRatio,主要是影响触发metaspaceGC的阈值

默认40,表示每次GC完之后,假设我们允许接下来metaspace可以继续被commit的内存占到了被commit之后总共committed的内存量的MinMetaspaceFreeRatio%,如果这个总共被committed的量比当前触发metaspaceGC的阈值要大,那么将尝试做扩容,也就是增大触发metaspaceGC的阈值,不过这个增量至少是MinMetaspaceExpansion才会做,不然不会增加这个阈值

这个参数主要是为了避免触发metaspaceGC的阈值和gc之后committed的内存的量比较接近,于是将这个阈值进行扩大

一般情况下在gc完之后,如果被committed的量还是比较大的时候,换个说法就是离触发metaspaceGC的阈值比较接近的时候,这个调整会比较明显

注:这里不用gc之后used的量来算,主要是担心可能出现committed的量超过了触发metaspaceGC的阈值,这种情况一旦发生会很危险,会不断做gc,这应该是jdk8在某个版本之后才修复的bug

MaxMetaspaceFreeRatio

默认70,这个参数和上面的参数基本是相反的,是为了避免触发metaspaceGC的阈值过大,而想对这个值进行缩小。这个参数在gc之后committed的内存比较小的时候并且离触发metaspaceGC的阈值比较远的时候,调整会比较明显

元空间的特点

  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个加载器有专门的存储空间
  • 只进行线性分配
  • 不会单独回收某个类
  • 省掉了GC扫描及压缩的时间
  • 元空间里的对象的位置是固定的
  • 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉

元空间的内存分配模型

  • 绝大多数的类元数据的空间都从本地内存中分配
  • 用来描述类元数据的类(klasses)也被删除了
  • 分元数据分配了多个虚拟内存空间
  • 给每个类加载器分配一个内存块的列表。块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些
  • 归还内存块,释放内存块列表
  • 一旦元空间的数据被清空了,虚拟内存的空间会被回收掉
  • 减少碎片的策略

我们来看下JVM是如何给元数据分配虚拟内存的空间的 

你可以看到虚拟内存空间是如何分配的(vs1,vs2,vs3) ,以及类加载器的内存块是如何分配的。CL是Class Loader的缩写。

理解_mark和_klass指针

要想理解下面这张图,你得搞清楚这些指针都是什么东西。

JVM中,每个对象都有一个指向它自身类的指针,不过这个指针只是指向具体的实现类,而不是接口或者抽象类。

对于32位的JVM:

_mark : 4字节常量

_klass: 指向类的4字节指针 对象的内存布局中的第二个字段( _klass,在32位JVM中,相对对象在内存中的位置的偏移量是4,64位的是8)指向的是内存中对象的类定义。

64位的JVM:

_mark : 8字节常量

_klass: 指向类的8字节的指针

开启了指针压缩的64位JVM: _mark : 8字节常量

_klass: 指向类的4字节的指针

Java对象的内存布局

类指针压缩空间(Compressed Class Pointer Space)

只有是64位平台上启用了类指针压缩才会存在这个区域。对于64位平台,为了压缩JVM对象中的_klass指针的大小,引入了类指针压缩空间(Compressed Class Pointer Space)。

压缩指针后的内存布局

指针压缩概要

  • 64位平台上默认打开
  • 使用-XX:+UseCompressedOops压缩对象指针 "oops"指的是普通对象指针("ordinary" object pointers)。 Java堆中对象指针会被压缩成32位。 使用堆基地址(如果堆在低26G内存中的话,基地址为0)

  • 使用-XX:+UseCompressedClassPointers选项来压缩类指针

  • 对象中指向类元数据的指针会被压缩成32位

  • 类指针压缩空间会有一个基地址

元空间和类指针压缩空间的区别

  • 类指针压缩空间只包含类的元数据,比如InstanceKlass, ArrayKlass 仅当打开了UseCompressedClassPointers选项才生效 为了提高性能,Java中的虚方法表也存放到这里 这里到底存放哪些元数据的类型,目前仍在减少

  • 元空间包含类的其它比较大的元数据,比如方法,字节码,常量池等。

元空间内存管理

元空间的内存管理由元空间虚拟机来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。话句话说,只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉。 
准确的来说,每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾回收器标记为不再存活,其对应的元空间会被回收。在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定Java引用。 
元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个全局的空闲组块列表。当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。当一个类加载器不再存活,那么其持有的组块将会被释放,并返回给全局组块列表。类加载器持有的组块又会被分成多个块,每一个块存储一个单元的元信息。组块中的块是线性分配(指针碰撞分配形式)。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。

上图展示的是虚拟内存映射区域如何进行元组块的分配。类加载器1和3表明使用了反射或者为匿名类加载器,他们使用了特定大小组块。 而类加载器2和4根据其内部条目的数量使用小型或者中型的组块。

Metaspace调优

使用-XX:MaxMetaspaceSize参数可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少。-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小。

MaxMetaspaceSize的调优

  • -XX:MaxMetaspaceSize={unlimited}
  • 元空间的大小受限于你机器的内存
  • 限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。如果怀疑有类加载器出现泄露,应当使用这个参数;32位机器上,如果地址空间可能会被耗尽,也应当设置这个参数。
  • 元空间的初始大小是21M——这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。
  • 如果启动后GC过于频繁,请将该值设置得大一些
  • 可以设置成和持久代一样的大小,以便推迟GC的执行时间

CompressedClassSpaceSize的调优

  • 只有当-XX:+UseCompressedClassPointers开启了才有效
  • -XX:CompressedClassSpaceSize=1G
  • 由于这个大小在启动的时候就固定了的,因此最好设置得大点。
  • 没有使用到的话不要进行设置
  • JVM后续可能会让这个区可以动态的增长。不需要是连续的区域,只要从基地址可达就行;可能会将更多的类元信息放回到元空间中;未来会基于PredictedLoadedClassCount的值来自动的设置该空间的大小

正如前面提到了,Metaspace VM管理Metaspace空间的增长。但有时你会想通过在命令行显示的设置参数-XX:MaxMetaspaceSize来限制Metaspace空间的增长。默认情况下,-XX:MaxMetaspaceSize并没有限制,因此,在技术上,Metaspace的尺寸可以增长到交换空间,而你的本地内存分配将会失败。

每次垃圾收集之后,Metaspace VM会自动的调整high watermark,推迟下一次对Metaspace的垃圾收集。

这两个参数,-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio,类似于GC的FreeRatio参数,可以放在命令行。

Metaspace可以使用的工具

针对Metaspace,JDK自带的一些工具做了修改来展示Metaspace的信息:

  • jmap -clstats :打印类加载器的统计信息(取代了在JDK8之前打印类加载器信息的permstat)。
  • jstat -gc :Metaspace的信息也会被打印出来。
  • jcmd GC.class_stats:这是一个新的诊断命令,可以使用户连接到存活的JVM,转储Java类元数据的详细统计。

示例1:jmap -clstats 

[ciadmin@2-103test_app pos-gateway-cloud]$ jmap -clstats 26964
Attaching to process ID 26964, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
finding class loader instances ..done.
computing per loader stat ..done.
please wait.. computing liveness..................................................................................liveness analysis may be inaccurate ...
class_loader    classes    bytes    parent_loader    alive?    type

<bootstrap>    2699    4611703      null      live    <internal>
0x00000000a1013a00    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a3e931e8    1    880      null      dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a083d280    1    1471    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a1c057c8    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a1013938    1    1474    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a1013d38    1    1471    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a141ae78    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a083d1b8    1    1473    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a163c658    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a293afa8    1    1473    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a19ec0a0    15    70893    0x00000000a001b938    live    com/aliyun/openservices/shade/com/alibaba/fastjson/util/ASMClassLoader@0x000000010066b7a0
0x00000000a2778848    1    1474    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a141a900    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a083d8c0    1    1473    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
0x00000000a163c720    1    880    0x00000000a001b938    dead    sun/reflect/DelegatingClassLoader@0x0000000100009df8
...
0x00000000a094fe68    0    0    0x00000000a0007438    live    java/net/URLClassLoader@0x000000010000ecd0

total = 177    12836    20539140        N/A        alive=9, dead=168        N/A    
[ciadmin@2-103test_app pos-gateway-cloud]$

猜你喜欢

转载自blog.csdn.net/weixin_42073629/article/details/110730567