元空间的详解

Metaspace

内存管理概述

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

Metaspace VM使用一个块分配器(chunking allocator)来管理Metaspace空间的内存分配。块的大小依赖于类加载器的类型
Metaspace VM中有一个全局的可使用的块列表(a global free list of chunks)--VirtualSpaceList。当类加载器需要一个块的时候,类加载器从全局块列表中取出一个块,添加到它自己维护的块列表中。当类加载器死亡,它的块将会被释放,归还给全局的块列表。

块(chunk)会进一步被划分成blocks,每个block存储一个元数据单元(a unit of metadata)。Chunk中Blocks的是分配线性的(pointer bump)。这些chunks被分配在内存映射空间(memory mapped(mmapped) spaces)之外。在一个全局的虚拟内存映射空间(global virtual mmapped spaces)的链表,当任何虚拟空间变为空时,就将该虚拟空间归还回操作系统
在这里插入图片描述

内存管理细节介绍

上面我们简单的介绍了元空间是如何分配内存的,然后我们讲一下元空间的具体的分配内存的策略

类装入器从Metaspace请求内存以获取一段元数据(通常是少量的,大约几十或几百个字节),比如200个字节。它将得到一个Metachunk——一块通常比请求的内存大得多的内存。

为什么?因为直接从全局VirtualSpaceList分配内存非常昂贵。VirtualSpaceList是一个全局结构,需要锁定。我们不想经常这样做,所以会给加载器一块更大的内存——这个Metachunk——加载程序将使用它更快地满足将来的分配,同时不锁定其他加载程序。只有当块用完时,加载程序才会再次困扰全局VirtualSpaceList。

元空间分配器如何决定要交给加载器的块有多大?这取决于类加载器

  1. 新启动的标准加载程序将获得小的4K块,直到达到任意阈值(4),在该阈值时,元空间分配器明显地失去了耐心,并开始给加载程序提供更大的64K块。
  2. 引导类加载器被称为加载程序,它倾向于加载许多类。所以分配器从一开始就给它一个巨大的块(4M)。这可以通过InitialBootClassLoaderMetaspaceSize进行调整。
  3. 反射类加载器(jdk.internal.reflect.DelegatingClassLoader)和匿名类的类装入器已知只能加载一个类。因此,他们从一开始就得到非常小的(1K)块,因为假设他们很快就不再需要元空间,再给他们任何东西都是浪费。
    请注意,整个优化——在假定加载程序很快就会需要它的情况下,为它提供比当前需要更多的空间——是对该加载程序未来分配行为的赌注,可能是正确的,也可能是不正确的。一旦分配器给它们一大块,它们就可能停止加载。

元空间体系结构

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

底层

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

底层以最大粗粒度通过虚拟内存调用的方式比如 MMAP,从操作系统预留和按需提交元空间,其结果就是一系列的大小为 2M(64 位平台)Region 节点组成的名为 VirtualSpaceList 的全局存储链表结构 。每个节点维护一个高水位线(High Water Mark),分割已提交的内存和未提交的内存。当分配空间达到高水位线的时候,再分配新的页空间。这个过程一直进行到节点空间用完,然后再生成新的节点并加入到全局链表结构中,老的节点也就退役了。节点内部分配的内存块成为 MetaChunk,通常有 3 种类型的尺寸:1k/4k/64k。
在这里插入图片描述
VirtualSpaceList及其节点是全局结构,而Metachunk由一个类装入器拥有。因此,VirtualSpaceList中的单个节点可能包含来自不同类装入器的块:
在这里插入图片描述
当一个类装入器及其所有相关的类被卸载时,用于保存其类元数据的元空间将被释放。所有现在可用的块都添加到全局可用列表(ChunkManager):

在这里插入图片描述

中间层

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

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

上层

上层是类加载对 Chunk 再次分割成更小的块给应用程序
在 MetaChunk 内部还有个二级类加载分配器,它将 MetaChunk 分割成更小的分配单元 Metablock 提供给上层调用:
在这里插入图片描述
当它出生时,它只包含头。随后的分配只是在顶部分配。由于整块元数据都可以被释放,所以不能再依赖于整块的分配。

注意当前块的“未使用”部分:由于块属于一个类装入器,所以该部分只能由同一个装入器使用。如果加载程序停止加载类,那么这个空间实际上是浪费了。

内存回收细节介绍

当一个VirtualSpaceListNode中的所有块碰巧是空闲的时,该节点本身将被移除。该节点将从VirtualSpaceList中删除。它的空闲块从Metaspace空闲列表中移除。节点被取消映射,其内存返回给操作系统。节点被“清除”。
在这里插入图片描述
为了使一个节点中的所有块都是空闲的,拥有这些块的所有类装入器都必须已经死亡。

这是否可能在很大程度上取决于碎片化:

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

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

另外,请注意,部分Metaspace(压缩类空间)将永远不会释放回操作系统。

  • 内存由操作系统在2MB大小的区域中保留,并保存在全局链接列表中。这些地区承诺按需提供服务。
  • 这些区域被分割成块,然后交给类装入器。块属于一个类装入器。
  • 块被进一步分割成微小的分配,称为块。这些是分发给呼叫者的分配单元。
  • 当一个全局块被重新使用时,它拥有一个全局块。部分内存可能会被释放到操作系统中,但这在很大程度上取决于碎片化和运气。

元空间的内存回收问题

为了使一个节点中的所有块都是空闲的,拥有这些块的所有类装入器都必须已经死亡。

这是否可能在很大程度上取决于碎片化: 一个节点的大小是2MB;块的大小从1K-64K不等;通常每个节点的负载是150-200块。如果这些块都是由一个类装入器分配的,那么收集该装入器将释放节点并将其内存释放给操作系统。

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

另外,请注意,部分Metaspace(压缩类空间)将永远不会释放回操作系统。

内存由操作系统在2MB大小的区域中保留,并保存在全局链接列表中。这些地区承诺按需提供服务。
这些区域被分割成块,然后交给类装入器。块属于一个类装入器。
块被进一步分割成微小的分配,称为块。这些是分发给呼叫者的分配单元。
当一个全局块被重新使用时,它拥有一个全局块。部分内存可能会被释放到操作系统中,但这在很大程度上取决于碎片化和运气。

元数据 区存在的问题:内存碎片
元空间虚拟机采用了组块分配的形式,同时区块的大小由类加载器类型决定。类信息并不是固定大小,因此有可能分配的空闲区块和类需要的区块大小不同,这种情况下可能导致碎片存在。元空间虚拟机目前并不支持压缩操作,所以碎片化是目前最大的问题。

JVM实战

现象

问题现象:服务频繁出现FGC

分析

原因分析:

1)首先查看GC日志,发现出现FGC的原因是metaspace空间不够

对应GC日志:

Full GC (Metadata GC Threshold)

2)进一步查看日志发现元空间存在内存碎片化现象

对应GC日志:

Metaspace       used 35337K, capacity 56242K, committed 56320K, reserved 1099776K

这边简单解释下这几个参数的意义

  • used :已使用的空间大小
  • capacity:当前已经分配且未释放的空间容量大小
  • committed:当前已经分配的空间大小
  • reserved:预留的空间大小

从已经使用的空间大小和capacity的数据可以看出来,结合上文所说的元空间的内存回收问题,说明了元空间的分配以 chunk 为单位,即使一个 ClassLoader 只加载1个类,也会独占整个 chunk,所以当出现 used 和 capacity 两者之差较大的时候,说明此时存在内存碎片化的情况。

元空间主要适用于存放类的相关信息,而存在内存碎片化说明很可能创建了较多的类加载器,同时使用率较低。

结合dump文件
在这里插入图片描述
DelegatingClassLoader是反射类加载器,可以看见其被加载了3000多次,这是导致问题的原因,具体的解释看上文的元空间回收细节

原因

我们知道这个导致元空间内存不足的原因是因为我们使用大量的反射类加载器也就是我们使用了大量的反射,在使用mybatis的时候我们会使用动态代理里面有大量的反射来创建对象。
什么导致了反射的内存占用高:

当使用Java反射时,Java虚拟机有两种方法获取被反射的类的信息。它可以使用一个JNI存取器。如果使用Java字节码存取器,则需要拥有它自己的Java类和类加载器(sun/reflect/GeneratedMethodAccessor类和sun/reflect/DelegatingClassLoader)。这些类和类加载器使用本机内存。字节码存取器也可以被JIT编译,这样会增加本机内存的使用。如果Java反射被频繁使用,会显著地增加本机内存的使用。

Java虚拟机会首先使用JNI存取器,然后在访问了同一个类若干次后,会改为使用Java字节码存取器。这种当Java虚拟机从JNI存取器改为字节码存取器的行为被称为膨胀。幸运的是,我们可以通过一个Java属性控制这种行为。属性sun.reflect.inflationThreshold会告诉Java虚拟机使用JNI存取器多少次。如果设为0,则总是使用JNI存取器。由于字节码存取器比JNI存取器使用更多本机内存,当我们看到大量Java反射时,最好使用JNI存取器。我们只需要设置inflationThreshold属性值为0即可。但一般不这样做

解决方案

分析结论:反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。

优化策略:

1)适当调大 metaspace 的空间大小。

2)优化不合理的反射调用。例如最常见的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapstruct 替换。

参考:

  • https://blog.csdn.net/hellozhxy/article/details/80559374?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2-80559374-blog-80559419.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2-80559374-blog-80559419.pc_relevant_default&utm_relevant_index=5
  • https://blog.csdn.net/v123411739/article/details/123778478?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165232404116781685382935%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=165232404116781685382935&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-3-123778478-null-null.142v9pc_search_result_cache,157v4control&utm_term=%E7%A8%8B%E5%BA%8F%E5%91%98%E5%9B%A7%E8%BE%89+%E2%80%8B&spm=1018.2226.3001.4187
  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值