MetaSpace简介

MetaSpace

什么是MetaSpace?

openjdk使用Metaspace来存储class的元数据,它在java 虚拟机进程中占了很对一块非java堆内存

注意: JDK版本依赖,元空间自jdk8以来发生了很多变化,当本文没有明确表达的时候我们讨论的JDK11.

class元数据是jvm进程java classes在运行时期的一个描述,根本上来说,JVM处理 java class的任何信息,包括但不限于来自JVM class file format

例如:

注意: 我们将会在后面的博客中讨论这些细节,英语比较好的的大佬可以直接去听油管的Class MetaData: A User Guide

即使java.lang.Class是一个运行在Java 堆里面的对象,但是class元数据它本身不是一个java对象,也不运行在Java堆,他们运行在Java堆外的原生内存区域,这个区域呗叫做MetaSpace.

MetaSpace 什么时候分配?

来自MetaSpace的分配和类加载是一起的。

当一个类被加载和在JVM里它的运行时表示被准备时,MetaSpace 被它的类加载器分配用来存储这个类的元数据,如下图
在这里插入图片描述

什么时候 MetaSpace释放?

一个class被分配的元空间时被它的类加载器所拥有,它只有当这个类加载器自己被卸载的时候才会被释放,不是之前。

只有当这个class loader 加载的所有的类没有或者的实例时,没有指向所加载的类和 class loader,而且GC也运行了 MetaSapce才会释放。(参考:JLS 12.7. Unloading of Classes and Interfaces

在这里插入图片描述

内存经常被保留!

然而,释放MetaSpace并不一定意味着被释放的内存就会归还给操作系统。
所有的或者一部分内存还是会被保留在JVM,这部分内存对于未来的类加载而被重用,但是这时也就意味着没有使用的那部分内存在JVM进程里。

这部分被保留的没有使用的内存有多大取决于MetaSpace的分裂程度-元空间的使用部分和空闲部分的紧密程度。另外,一部分MetaSpace( Compressed Class Space)将根本不回归还给操作系统。

注意:详细的释放逻辑将会在下一片文章讲解
注意:关注到MetaSpace的占用时一个升级到JDK11的很好的理由, JDK11在这方面有很多的改进,包括减少碎片化和内存浪费。

MaxMetaspaceSize, CompressedClassSpaceSize

这两个参数时用来限制MetaSpace的大小的:

  • -XX:MaxMetaspaceSize 决定了MetaSpace被允许增长到的最大commit大小,默认时不做限制的。
  • -XX:CompressedClassSpaceSize 决定了MetaSpace很重要的一部分virtual的大小,Compressed Class Space,它默认的值时1G,(注意:reserved space, not committed).
    关于MetaSapce的大小的细节后面会讲

MetaSpace 和 GC

只有当GC运行和class loader 卸载时MetaSpace才被释放,所以在某种程度上来说,运行GC来清理旧的class 元数据时有意义的,即使在常规的JAVA堆中运行GC没有获得太大的收益,会有两种情况触发元空间诱导的GC:

  • 正在分配元空间: VM拥有一个阀值,通过触发一次GC,他没有首先尝试去收集老的类加载器,而是重用它的MetaSpace,MetaSpace就不会涨,这就是MetaSpace的GC阀值,这能保证MetaSpace不会增长即使没有释放老的元数据,这个阀值会上下移动,大致和commited内存保持一定的范围

  • 当遇到一次MetaSpace OOM -这个OOM发生在当commited的内存总和达到了MaxMetaSpaceSize时,或者运行超过了Compressed Class Space,GC企图纠正这种情况, 如果它实际上很好的卸载了class loader, 否则即使即使我们有足够的JAVA堆,仍然会进入糟糕的GC循环

参考:

  • https://stuefe.de/posts/metaspace/what-is-metaspace/
  • https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.15
  • https://www.cnblogs.com/mazhimazhi/p/13456515.html

MetaSpace 架构

我们继续探究MetaSpace的架构。

像大多数的重要的分配器一样,MetaSpace也是分层实现的。

在底层,很大的区域内存被分配的内存来自操作系统 , 在中间层我们切开哪些区域为一些不这么大的chunks并且把他们交给 class loaders。

在最上层,class loaders又将这些chunks切开来服务调用者代码块。
下面我们来根据不通的层级来了解下MetaSpace的细节

下层: 空间列表(The space list)

在最底层-在颗粒度最粗-被MetaSpace保留的和通过像mmap(3)的虚拟内存调用按需向操作系统申请内存,在64位平台每次是2MB的区域。

那些映射的区域被作为节点保存在一个名为VirtualSpaceList的全局链表里。
每个节点管理一个高水位,把以提交的空间和未提交的空间分离开。当内存分配到了一个高水位的时候一个新的内存页就会即刻提交。(保留一点余量是为了避免频繁的调用操作系统)??这句话没理解

这个过程会一直持续到这个节点被完全使用完,然后一个新的节点被分配并且加到VirtualSpaceList这个链表里,然后这个老的节点开始“退休”(退休节点留下内存空间并没有小时,但是被分割成快加到全局 freelist里)

内存被分配从一个节点到叫做MetaChunk的chunks,这些chunks有三种大小分别叫特殊的,小的和中等的,命名是有历史意义的,通常这三种对应的大小是1K/4K/64K(这个大小32位平台有所不同,以及这些chunk是位于CompressedClassSpace 或者 Non-Class Metaspace也不同)
模型如下:
VirtualSpaceList and VirtualSapceListNode

VirtualSpaceList 和它的节点们是全局的结构体,但是 Metachunk 确实是被一个class loader所持有,所以在VirtualSpaceList中一个单节点也可能经常包含来自不同class loader的chunks
如下图:
VirtualSapceListNode and class loader

当一个class loader和它相关联的class被卸载,MetaSpace过去持有的它的class metadata被释放。现在所有的空白chunks被加到全局 free list里(ChunkManager)
如下图:
ChunkManager

这些chunks被重用的情况: 如果另外一个class loader 开始加载别的class并且分配MetaSpace,这种情况可能会使用一个chunks来替代分配一个新的chunks:
如下图:
ChunkReused

中间层: MetaChunk

一个class loader 从MetaSpace请求一块内存用来存储MetaData(通常情况下很小-几十或者几百字节),他将获得一块MetaChunk代替–通常是比他请求的大得多的内存。

为什么?因为从全局的VirtualSpaceList 分配内存是非常昂贵的。VirtualSpaceList是一个全局的结构器而且需要加锁,我们不想这样做,所以一块更大的内存被分给了calssloader–那就是MetaChunk,loader会使用它来满足更快的未来的内存分配,和别的class loader并发使用,而且没有锁。只有当那个chunk被用完了class loader才会再一次干扰VirtualSpaceList

MetaSapce 分配器怎样决定多大内存交给class loader?就是一个猜测:

  • 一个新的标准的应用的class loader, 会拿到4K大小的chunks,直到使用到达4这个阀值是,MetaSpace 分配器会给这个laoder更大的64K的chunks。
  • bootstrap class loader(引导类加载器)倾向加载很多类,所以分配器给它了一个很大的chunk,从一开始起就是4M, 当然这是可用通过InitialBootClassLoaderMetaspaceSize来调节的。
  • Reflection class loaders (jdk.internal.reflect.DelegatingClassLoader)(反射类加载器)和匿名类加载器(不是真正的类加载器,但是现在来时这不重要),每次只加载一个class,所以一开始的时候他们只是被给了1K大小的chunks,因为假定他们会很快停止使用MetaSpace,所以再给他们任何东西都会被浪费。

注意这整个优化-- 在假定加载器很快就会需要的情况下,为它提供比当前需要更多的空间这种行为是一个对未来内存分配行为的赌注,而且这可能正确也可能不正确,当分配器给了他们一个很大的chunk的时候可能会直接停止加载。

上层结构: MetaBlock

在一个MetaChunk 中我们还有第二中分配器- 本地类加载器分配器,它把MetaChunk 划分为小的配额单元,这些单元被叫做MetaBlock,并且是实际分发给调用者的单元 (所以对于 实例来说一个MetaBlock拥有一个InstanceKlass)

这个本地类加载器分配器可以是简单的因此很快:

class metadata的生命周期被绑定到了class loader,class loader 死亡的时候 class metadata可以被大批量释放,所以JVM不需要关注随机释放的Metablocks

让我们来看看MetaChunk

Metachunk

当MetaChunk 产生的时候,它只包含了头部,后面的分配只被分配到顶部,??,而且分配器不需要太智能,因为它可以依赖整个Metadata被批量释放

注意当前chunk的“unused”部分:一旦这个chunk被class loader所持有,这部分未使用的区域只能被相同的class loader所使用,如果loader停止加载class,这部分空间实际上就被浪费掉了。

ClassloaderData和ClassLoaderMetaspace

class loader 把它的原生表示维护在一个叫ClassLoaderData的原生结构里。

ClassLoaderData 结构体持有了一个ClassLoaderMetaspace结构体的引用,这个ClassLoaderMetaspace是用来维护当前这个class loader正在使用的所有的MetaChunks的列表。

当一个loader被卸载的时候,这个loader相关联的ClassLoaderDataClassLoaderMetaspace会被删除,这会释放所有的这个class loader使用的chunks到MetaSpace FreeList里面去,如果条件正确,那么这些空闲的内存有可能会归还给操作系统也可能不会,详情如下。

匿名类(Anonymous classes)

ClassloaderData != ClassLoaderMetaspace

注意我们一直在说“Metaspace 内存被它的class loader所持有”,但是这里我们撒了点谎,这只是一种简化的说法,随着匿名类的加入这变得更加的复杂。

这里是一些被生成用来支撑动态语言特性的结构。当一个加载器加载一个匿名类,这个类它拥有独立的ClassLaoderData它的生命周期和匿名类一样,而不是这个类的class loader外壳(所以它相关的metadata可以在class loader外壳被收集之前收集),那也意味着一个class loader 有一个主要的加载所有普通类的ClassLoaderData,还有一个次要的针对每个匿名类的结构。

这中和别的东西分离的目的是为了避免不必要地延长lambda和Method句柄等metspace分配的生命周期。

结构如下图:
CLM&CLMS

所以最后再问一遍什么时候MetaSpace的内存会归还给操作系统啊?

再看这个问题,我们可以更详细的回答这个问题了:

当一个VirtualSpaceListNode内所有的chunks是空的,那个节点自身被删除。这节点会从VirtualSpaceList被移除,这节点的空闲的chunks将会从MetaSapce freelist移除,这节点变成未映射的节点,那么它的内存将会归还给操作系统, 这个节点被清除

如下图:

return memory

一个节点的所有chunks是空的,那么class loader 所拥有的所有chunks必须是已经死亡了。

下面的情况是和会发生取决于碎片化程度:

一个节点大小为2MB;chunks的大小从1K-64K不等;通常的负载是每个节点~150 - 200个chunks。如果这些chunks都已由单独的class loader分配,那么收集该class loader将释放节点并将其内存释放给操作系统。

但是,如果这些chunks由不同的class loader拥有,并且有不同的生命周期,则不会释放任何内容。当我们处理许多小型class loader时(例如,用于匿名类或反射委托的class loader)可能会出现这种情况。

另外,请注意,部分metspace(Compressed Class Space)将永远不会被释放回操作系统。

回顾总结:

  • 从操作系统中获取来的被保留的2MB大小的内存区域维护在一个全局的链表中。这样的空间是按需提交的。
  • 这些空间被分割为chunk,然后交给class loader,一个chunk属于一个class loader。
  • chunk被进一步分割为更小的 blocks,这些是交给调用者的分配单元。
  • 当一个class loader消亡,它所拥有的chunks被添加到全局空闲列表中并被重用。部分内存可能会被释放会操作系统,但是这取决于内存的碎片化程度和运气。

备注:鉴于我这个菜鸡英语和语文水品有限有些地方有可能翻译不准确,还望大佬一起讨论

参考:

  • https://stuefe.de/posts/metaspace/metaspace-architecture/
  • https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/hotspot/share/memory/metaspace
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值