译|深入理解Metaspace

    文章目录

    • 什么是 Metaspace?

    • 何时分配 Metaspace?

    • 何时释放 Metaspace?

    • Metaspace 体系结构

    • 匿名类的元空间释放

    • 对象内存布局

    • 压缩指针

    • 压缩对象指针

    • 压缩类指针

    • 压缩指针开启

    • 元空间配置

    • 参考

Java 虚拟机内存区也就是运行时数据区划分为线程共享的数据区和线程隔离的数据区,见下图蓝色区域和绿色区域。

译|深入理解Metaspace

其中线程共享的数据区由逻辑连续的内存块—堆区和方法区组成,前者用于存放对象实例,后者用于存放虚拟机加载的类信息、常量、静态变量和即时编译后的代码等。为了对这两块内存区域进行管理JVM 将其分为新生代、年老代和持久代。其中新生代、年老代在堆区中,为了让垃圾回收器可以像管理 Java 堆一样管理这部分内存,省去专门为方法区开发内存管理的工作,HotSpot 将其纳入堆的逻辑部分统一管理,尽管它属于非堆区。

但同时也带来了问题:更容易导致内存溢出,为了优化对持久代的参数设置和简化垃圾回收,JDK 1.8 开始持久代被基于本地内存(Native Memory)存储的 Metaspace 取代。

什么是 Metaspace?

Metaspace 是 VM 虚拟机存储 Class Metadata 的内存,而 class metadata 是 Java Class 在 JVM 进程中的运行时表示,包括但不仅限于 JVM 类文件格式中表示的数据。关于完整的 JVM 类文件格式可以参考:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

何时分配 Metaspace?

Metaspace 的分配伴随着类的加载。当类的运行时表示在 JVM 中准备完毕,类加载器就会为其分配元空间。如下图所示,沿着纵轴随着时间推移类加载器 Id 在元空间分配了存储类 X 和 Y 的元数据。

译|深入理解Metaspace

注意:此时类加器对象 Id 本身也存储在堆中,暂时不关心类加载器的元数据。

何时释放 Metaspace?

因为类加载器分配并拥有了类的元空间,理应在类加载器卸载前,元空间不会释放。也就是说当类加载器的所有类实例没有活跃对象,并且没有对这些类实例和类加载器的引用时,才会进行垃圾回收。如下图沿着纵轴,随着时间推移,当类 X 和 Y 以及他们的类加载器 Id 没有实例和对其的对象引用,此时分配给 X 和 Y 的元空间也将被回收。有关类卸载的简要说明可以参考:

https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.7

译|深入理解Metaspace

注意:释放的元空间并不会返还给操作系统,而是会保留重用。下文中会详细讨论元空间结构。

那何时进行垃圾回收?元空间在垃圾回收器卸载类加载器之后,而垃圾回收的时机有两种情况:

  1. 分配元空间的时候。当虚拟机 VM 到达不释放旧的类加载器就无法再分配新的元空间的阈值时候就会触发 GC,然后重用释放的元空间。

  2. Metaspace OOM。当提交的内存达到阈值 MaxMetaspaceSize(最大提交的元空间大小) 或者 Compressed Class Space(默认 1G 的预留压缩类空间)空间用完会导致 OOM 异常。此时就会导致 GC。这两个参数下文详细介绍。

Metaspace 体系结构

Metaspace 按照分层结构组织成 3 层:

1. 底层是从操作系统分配的大的 Region 区域

底层以最大粗粒度通过虚拟内存调用的方式比如 MMAP,从操作系统预留和按需提交元空间,其结果就是一系列的大小为 2M(64 位平台)Region 节点组成的名为 VirtualSpaceList 的全局存储链表结构 。每个节点维护一个高水位线(High Water Mark),分割已提交的内存和未提交的内存。当分配空间达到高水位线的时候,再分配新的页空间。这个过程一直进行到节点空间用完,然后再生成新的节点并加入到全局链表结构中,老的节点也就退役了。节点内部分配的内存块成为 MetaChunk,通常有 3 种类型的尺寸:1k/4k/64k。

译|深入理解Metaspace

VirtualSpaceList 和节点是全局存储结构,然而 MetaChunk 为类加载器所有,于是在节点内部,就可能包含属于不同类加载器的 chunk:

译|深入理解Metaspace

当累加器和所有的类都卸载之后,分配给该类加载器的元空间也将释放并被加入全局自由列表FreeList:

译|深入理解Metaspace

当其他类加载器加载类并分配元空间的时候,可能就会从这些自由列表中重用 chunk 而不是直接分配新的存储块:

译|深入理解Metaspace

2. 中间层是给类加载的由 Region 切割成不太大的 Chunk 块

类加载器从元空间请求的元数据存储空间通常比它真实需要的要大一些,并以 Metachunk 的形式返回。这是因为从全局 VirtualSpaceList 分配空间比较昂贵且需要加,而直接分配稍大一些的空间有助于将来需要存储空间直接从加载器本身获取而不是再次请求 VirtualSpaceList ,只有当 chunk 用完之后才需要再次请求 VirtualSpaceList 。

注意:给类加载器比请求更多的空间是基于未来不久会再次分配空间的假设 ,但是这个假设不一定正确,当类加载之后不再申请分配空间,此时就会浪费空间。

3. 上层是类加载对 Chunk 再次分割成更小的块给应用程序

在 MetaChunk 内部还有个二级类加载分配器,它将 MetaChunk 分割成更小的分配单元 Metablock 提供给上层调用:

译|深入理解Metaspace

注意:这里当前 chunk 的 unused 部分,它被一个类加载器拥有,也只能分配给它加载的类,当它不再加载类的时候,这部分空间也就浪费掉了。

匿名类的元空间释放

类加载器用ClassLoaderData 保存其本地表示(Native Representation)。它引用一个保存类加载在使用中的 Metachunks 列表的数据结构 ClassLoaderMetaspace。

当类加载器卸载之后,相关的 ClassLoaderData ClassLoaderMetaspace 也将被删除,并把所有 chunk 放入全局自由列表(Metaspace freelist)。上文提到这些自由列表供后续重用,而不会归还操作系统,这在涉及到匿名类加载等情况下是不正确的:当加载匿名类的时候,它有自己的 ClassLoaderData ,其生命周期跟匿名类绑定而不是类加载器,于是匿名类及其元数据可以在类加载之前被回收,也就意味着类加载器有两种 ClassLoaderData ,加载正常类的主 ClassLoaderData (primary)和加载匿名类的辅 ClassLoaderData (secondary),见下图:

译|深入理解Metaspace

于是问题来了,chunk 内存空间能否(何时)归还给操作系统?当一个节点的所有 chunk 都自由了(free),节点本身就会从 VirtualSpaceListNode 移除,这些自由 chunk 也将被移除,节点也就解除内存映射会释放空间到操作系统。但是一个节点 2M,chunk 大小 1k-64k 大小不等,每个节点大概 150~200 个 chunk,只有当所有的 chunk 都属于同一个类分配的,回收加载器才会释放节点并返还操作系统,实际情况比如有很多小的类加载器如匿名类加载器或者反射代理等,很可能存在这些 chunk 被不同的加载器拥有,那么就不会有自由 chunk 也就不会释放节点了。另外元空间中的压缩类空间部分也不会返还给操作系统。关于 Metaspace 再总结一下:

  1. 内存按照大小 2M 的 region 预留并被记录在全局链表结构中。这些 region 按需提交。

  2. region 被分割成 chunk,交给类加载器二次分配。每个 chunk 属于一个类加载器。

  3. region 进一步分割成更小的分配单元 block 给上层应用调用者。

  4. 当类加载器被回收的时候,拥有的 chunk 被加入全局自由列表以重用,部分内存可能会返还操作系统,但是要看分割块大小和运气。

对象内存布局

对象在内存中的表示由 3 部分组成:对象头、实例数据和对齐填充部分(padding)。

译|深入理解Metaspace

其中对象头由 Mark Word 和 Klass 组成,前者包含对象运行时的状态信息;后者为指向在堆外部(Metaspace)存储的类的元数据的指针,两者的长度跟机器位数有关:32 位机器 4 个字节,64 机器 8 个字节。如果对象是数组,对象头中额外包含记录数组长度的信息。在对象头之后存储对象的域信息,由于 Java 虚拟机默认存储需要按照 8 字节对齐,不足部分需要填充补齐。比如以下语句:

Integer i = new Integer(23);

在 32 位机器上,占用空间大小为 4(对象指针 i)+4(mark word)+4(类指针)+4(int 实例)=16 字节,而在 64 位机器上占用空间大小为 8+8+8+4=28 字节。

可见更大的位数带来更大的存储消耗,同时 32 位机器理论最大能寻址 2 的 32 次方也就是 4G 的空间,64 位理论最大寻址空间 2 的 64 次方。那是不是位数越大越好呢?也不是,因为:

  1. 64 位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了 GC 的发生,更频繁的进行 GC;

  2. 64 位对象引用增大了,CPU 能缓存的对象指针将会更少,从而降低了 CPU 缓存的效率。为了保证 32 位的性能,继续使用 32 位寻址能力,但如何在 64 位机器上用 32 位地址引用超过 4G 的地址空间呢?答案是压缩指针。基本原理就是通过基地址和偏移量换算真实的地址,详见下文。

压缩指针

所谓压缩针指的是指针地址本身,而不是指向的对象,因此 64 位指针压缩后为 32 位。压缩指针有两种:Compressed Object Pointers 和 Compressed Class Pointers。

压缩对象指针

压缩对象指针(Compressed Object Pointers 简称 CompressedOops,oops: ordinary object pointe)用 32 位地址寻址 64 位的空间。因为对象分配在堆中,所以当堆空间小于 4G,即使是 64 位的地址也可以用其低 32 位完全表示,当堆空间大于 4G 但小于 32G,地址映射读进 32 位地址到 64 位寄存器,左移 3 位,低 3 位补 0,相当于将地址扩大了 8 倍,原来在 32 位地址空间的地址比如 0x1、0x2 变成了 64 位地址空间的 0x8 和 0x10,当从寄存器读出 64 地址,再将地址右移 3 位,还原原来 32 位地址。

这样一来 32 位地址就可以寻址最大 32G 了,然而这样就没法寻址不能被 8 整除的地址了,其实上文提到虚拟机默认按照 8 字节对齐了,所以虚拟机还是可以引用到任何 32G 内的地址。同样的思路,如果移动 4 位,虚拟机可以寻址最大 64G 空间,但是此时就需要按照 16 字节对齐,在堆中保存压缩指针所节约的成本被对齐对象而浪费的内存抵消了,因此对于大部分处理器,按照 8 字节对齐是最优的。所以当堆空间大于 32G,就没必要压缩指针了,直接用 64 位地址好了。

注意:压缩指针的压缩范围包括:

  1. 每个 Class 的属性指针(静态成员变量)

  2. 每个对象的属性指针

  3. 普通对象数组的每个元素指针

针对一些特殊类型的指针,JVM 是不会优化的。比如指向 PermGen 的 Class 对象指针、本地变量、堆栈元素、入参、返回值、NULL 指针不会被压缩。

压缩类指针

压缩类指针( Compressed Class Pointers)指向的地址空间为压缩类空间(Compressed Class Space)。压缩类空间的寻址方式跟堆对象方式一致,也是通过基地址和偏移量移位映射。对压缩类空间的要求也是大小不超过 32G 并且要求压缩类空间是连续的 region。这个要求只针对 Kclass(class space)部分,对非 Klass 的其他部分(non-class space)则无此要求,于是前文介绍的元空间存储结构需做修改才能支持这种差异:实现方式为将原来的全局链表(VirtualSpaceList )和全局自由列表(ChunkManager )实现 2 份,Kclass 和非 Kclass 各有一份。

又因为 Klass 需要连续的地址空间,此时链表结构就退化为只有一个节点的结构且不能增长。这个节点就是压缩类空间,默认大小 1G。非类空间和类空间的全局链表结构对比如下图:

译|深入理解Metaspace

而 ChunkManager 和 ClassLoaderMetaspace (记录类加载器在使用的 chunk)因为从节点内部再分配,没有变化,如下图:

译|深入理解Metaspace

压缩指针开启

开启对象压缩指针通过 +UseCompressedOops,开启压缩类指针通过+UseCompressedClassPointers。由于 UseCompressedClassPointers 的开启是依赖于 UseCompressedOops 的开启,因此,要使 UseCompressedClassPointers 起作用,得先开启 UseCompressedOops。虽然开启 UseCompressedOops 会默认开启 UseCompressedOops,但可以通过-UseCompressedClassPointers 关闭压缩类指针。

继续计算上面提到的 Integer 对象大小:

  • 在 64 位机器上,开启压缩类指针后大小为:4(对象指针 i)+8(mark word)+4(类指针)+4(int 实例)=20B。

元空间配置

MaxMetaspaceSize 和 CompressedClassSpaceSize 是控制元空间大小的两个抓手。MaxMetaspaceSize 包含 Kclass 和非 Klass 两部分,决定了最大可提交的空间大小,由于元空间使用本地内存,默认无上限。CompressedClassSpaceSize 决定了压缩类空间的虚拟大小,应用一旦启动该值无法修改,默认 1G。这两个参数限制的空间如下图:

译|深入理解Metaspace

上图中元空间包括 non-class space 和 class space 两部分,每个部分都由已经提交和未提交的部分组成,分别用红色和蓝色表示。前者是一个由 region 组成的节点列表,后者是由一个更大的单 region 节点组成。

元空间的红色的提交部分之和由 -XX:MaxMetaspaceSize 控制,如果提交内存超限,将抛出  OutOfMemoryError(“Metaspace”) 异常。而元空间内部的 class space 由提交和未提交的部分组成,大小受限于  -XX:CompressedClassSpaceSize ,如果超限,将抛出  OutOfMemoryError(“Compressed Class Space”) 异常。

当类加载时候就需要从非 Kclass 空间和 Kclass 分配空间,总的元空间大小默认无上限,但是类空间大小是确定的,那是否会发生超限抛异常呢?还是先看下两部分空间到底存储什么吧:

译|深入理解Metaspace

由上图可知,class space 由 Klass 结构、分别记录成员方法和接口方法的 vtable 和 itable 以及描述类中引用的对象位置的 map 结构组成,non-class space 由常量池、方法元数据、控制 JIT 的运行时方法数据以及注解等组成,其中方法元数据 ConstMethod 结构包括方法字节码、局部变量表、异常表、参数信息以及方法签名等。

根据对典型应用中 bootstrap 和 app 加载的正常类得出的统计结果表明平均每个类需要约 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space。对于匿名类,该比例为 1k Non-Class Space 和 0.5k Class Space。值得一提的是,虽然匿名类很小,但是 class space 并不少,这是因为诸如 Lambda 类虽然很小,但是它的 Klass 结构不可能小于 sizeof(Klass)。由此我们可以推断,不设置元空间大小限制,在理想环境下,最多可以装载 150 万个正常类。那还有没有必要设置元空间大小限制?如果要限制,原因无外乎两点:

  1. 告警系统需要。系统维护人员需要知道什么时候元空间消耗比预期大。

  2. 限制虚拟内存大小。通常我们感兴趣的是实际消耗内存,但是虚拟内存大小可能会导致虚拟机进程达到系统限制。

如果要限制 Metaspace 大小使得系统更容易被监控,同时不用在乎虚拟地址空间的大小,那么最好只设置 MaxMetaspaceSize 而不用设置 CompressedClassSpaceSize。如果不设置,那么 CompressedClassSpaceSize 默认为 MaxMetaspaceSize 的 80% 左右,这也许可以多补偿一些 class space。但要注意,这只是预留空间而不是提交的空间,class space 也不能设置太低,太低的话,CompressedClassSpaceSize 会先于 MaxMetaspaceSize 达到阈值,大多数情况, 两者占比为 1 比 2 比较安全(CompressedClassSpaceSize = MaxMetaspaceSize / 2)。

那如何设置 MaxMetaspaceSize 呢?

根据前面统计的平均类占用大小(~5-7k non-class space,600-900 bytes class space,比例接近 8 比 1),按照每个类 8k non-class space,1k class space,乘以加载的类数量就可以得出总的元空间大小。比如你的应用程序计划加载 10000 个类,那么从理论上讲,你只需要 10M 的 Class Space 和 80M Non-Class Space。然后考虑安全系数,比如因子 2 在大多数情况下比较安全,那么需要 20M 的 Class Space 和 160M 的 Non-Class Space,也就是总大小为 180M。因此,在这里 -XX:MaxMetaspaceSize=180M 是一个很好的选择。

参考

  • https://stuefe.de/posts/metaspace/what-is-metaspace/

  • https://shipilev.net/jvm/anatomy-quarks/23-compressed-references/

原文 

http://mp.weixin.qq.com/s?__biz=MzI3ODU3MzQ2Ng==&mid=2247484015&idx=1&sn=b19f7fa24353a1d91a3a588836e61d4b

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 译|深入理解Metaspace

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址