一文就带你把JVM并发标记清除回收,元数据内存管理给一次性搞懂

元数据内存管理

从JDK 8开始,元数据从堆空间中被移除,放入本地内存中。为了更好地管理元数据空间,JVM也设计了一套独立的内存分配和回收的实现。在JDK 16之前的实现中,元数据内存管理的底层实现也使用二叉树的内存块管理,使用的数据结构与CMS老生代中大块自由空间的数据结构完全相同。所以本节介绍一下元数据内存的管理。

元数据区(也称为Metaspace)是JVM中一块非常重要的内存空间,应用运行时遇到元数据空间不足的情况会直接触发Full GC,对性能会产生一定的影响。

内存管理

Metaspace和类加载数据(Class Loader Data,CLD)关联,简单地讲,每一个CLD都有一个Metaspace,CLD加载的所有类产生的元数据都在其对应的Metaspace中管理。

整个JVM中所有的Metaspace使用的内存都通过VirtualSpaceList(简称VLS)管理,而VSL中的每一个节点(Node)使用VirtualSpaceNode(简称VSN)管理。使用VSN的目的是便于回收内存。数据整体结构图如图4-34所示。

图4-34 元数据空间内存管理整体结构

每一个Node(即VSN)和VirtualSpace关联,VirtualSpace的内存使用总是按照顺序从头开始。但是类元数据的大小并不相同,导致Metaspace管理的内存块是多样化的,在类不再使用时还可以被卸载,占用的空间可以被回收。为了更好地管理元数据的分配和回收,在VSN中引入了以下两类结构:

1)固定大小的结构,称为chunk。目前有3种chunk大小,分别是128B、512B和8KB。

2)二叉树,管理超过8KB大小的内存块。

VSN和VirtualSpace的关系如图4-35所示。

图4-35 VirtualSpaceNode和VirtualSpace关系示意图

在图4-35中还有一个ChunkManager(简称CM)也是全局共享的,主要目的是当类被卸载时对应的Metaspace会被释放。但可能其使用的VSN中还存储其他的类元数据,所以VSN无法被回收,因此会将释放的元数据内存放入ChunkManager中,供后续的元数据分配使用。

Metaspace中有一个成员变量SpaceManager,用于管理本Metaspace对应的CLD真正使用的内存块(包括固定大小的内存块和二叉树中的内存块)。在SpaceManager中还有一个BlockFreeList的链表,用于保存一些零碎的内存(这些内存通常来自类加载失败或者类因重定义被重新加载而释放的内存)。

分配

Metaspace的内存分配过程大体可以分为如下几步:

1)尝试从BlockFreeList中进行分配,但是由于BlockFreeList对于小微内存直接使用链表方式管理,空间不连续,因此分配成本比较高。在JVM中会设置一定的条件,当满足这些条件时才能从BlockFreeList中分配。

2)无法从BlockFreeList中分配时,从Metaspace中正在使用的chunk中分配,这个chunk相当于缓存,用于加速分配。

3)没有可用的chunk时会分配一个chunk再响应分配。分配chunk的逻辑相当复杂:优先从ChunkManager中重用已经释放的chunk,如果无法找到合适的chunk,则需要从VirtualSpaceNode中分配;如果VirtualSpaceNode也无法满足分配需求,会扩展VirtualSpaceList的大小再来分配。

VirtualSpaceList的扩展是指为VirtualSpaceList分配新的VirtualSpaceNode。每个VirtualSpaceNode的大小为256KB(无法在运行时态调整其大小),将VirtualSpaceNode限制为256KB的目的是便于回收VirtualSpaceNode,当整个VirtualSpaceNode没有任何chunk时就可以被回收。

回收

理解了Metaspace的分配以后,其回收过程就不难理解了。由于Metaspace和CLD相关联,当CLD被卸载以后就可以执行Metaspace的回收。

Metaspace的回收首先是将卸载的类元数据内存块归还到ChunkManager中,以便后续再利用。在这一步中并不会真正释放内存,仅仅是将内存归还到ChunkManager中,以便其再次被利用。而Metaspace真正的回收是针对VirtualSpaceNode的回收,当且仅当VirtualSpaceNode中所有的chunk都是空闲的才能被释放,而这样的情况并不常见。通常来说,Metaspace的回收仅仅是将类元数据占用的空间释放再利用,很少能真正地回收内存。

在实际工作中可能会遇到Metaspace频繁触发Full GC的情况,通常有两种可能:一是Metaspace空间太小,无法满足应用的需要;二是Metaspace碎片化率非常高,导致内存利用率不高。对于这两种情况,目前并没有特别好的解决方法,一方面要在应用中尽可能避免大量地、无限制地使用反射等消耗元数据空间的操作,另一方面可以考虑设置更大的元数据空间。

元数据管理的优化

在JDK 16中正式合入一个关于元数据的补丁(patch),用于优化元数据的管理,详细内容参考JEP 387[1]。这个特性本质上最大的改变是:

ChunkManager中chunk使用伙伴存储来管理。伙伴存储管理是一个非常经典的内存管理技术,它分配速度快,造成的内存碎片少。

伙伴存储可以简单地概括如下将内存块大小按照2的幂次划分,假设内存块从上向下逐步变小,上一层的内存块的大小等于下一层两个内存块的大小,在下一层内存不足时可以从上一层获取一个内存块并拆分为下一层的两个内存块,当下一层两个空闲的内存块连续时可以合并到上一层变为一个内存块。

元数据的chunk中共划分为13层,最大的内存块是第0层,为4MB,最小的内存块是第12层,为1KB。每一层都是一个FreeList,管理相同大小的内存块,如图4-36所示。

图4-36 伙伴存储示意图

使用新的管理方式来分配元数据,通常根据内存需要的大小从期望的level的FreeList中分配内存。当无法满足分配时,会从更大的内存块中尝试分配,这就涉及内存块的拆分。在JDK 16的实现中,按照如下顺序进行内存分配:

1)从level、level+1、level+2中依次请求分配内存,此时查找的是已经使用的内存块。

2)如果不成功,从level0中依次请求分配内存,此时查找的是已经使用的内存块。

3)如果不成功,再次尝试,从level12中依次请求分配内存,此时查找的是已经使用的内存块。

4)如果不成功,从level12中依次请求分配内存,此时查找的是是否存在完全未使用的内存块。

5)如果不成功,从level0中依次请求分配内存,此时查找的是是否存在完全未使用的内存块。

6)如果不成功,重新请求一个VirtualSpaceNode(大小为8MB),可以拆分成两个第0层的内存块,再次尝试分配。

另外,通常情况下元数据的分配是从chunk中获得的,但是在一些特殊的场景中,例如类加载失败或者类因重定义被重新加载,可能会导致需要释放内存。将这些内存单独管理起来并且重用,可以有效地提高内存使用的效率。使用FreeBlock的方式管理释放的内存,在FreeBlock中采用BinList和BinTree管理释放的内存。

本文给大家讲解的内容是JVM垃圾回收器详解:并发标记清除回收,元数据内存管理

  1. 下篇文章给大家讲解的内容是JVM垃圾回收器详解:并行回收的内存管理
  2. 感谢大家的支持!
展开阅读全文

页面更新:2024-03-12

标签:数据   可能会   标记   加载   分配   大小   内存   伙伴   两个   情况   空间

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top