跳至主要內容

JVM(十七)Metaspace 解密

安图新大约 7 分钟

JVM(十七)Metaspace 解密

一:Metaspace 介绍

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

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


二: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 是由一块块内存慢慢组合起来的,在没有达到限制条件的情况下,会不断加长这条链,让它可以持续工作。

CompressedClassSpace 和 metaspace 之间的关系

虽然前面提到CompressedClassSpace由参数-XX:CompressedClassSpaceSize参数来控制,而且默认 1G,但是这并不意味着这块内存的大小不设置就是 1G 的大小了。我们先来做个小测试。

1:VM Options 不写任何参数

 
 
 
 

我们使用下面的指令找到这个 pid

jps -l

然后使用下面命令指令查看参数的默认值

jinfo -flag CompressedClassSpaceSize 32224 

 

查出来的值换算后刚好是 1G。

最小最大的 Metaspace 大小如下,最大值可以认为无穷大了,最小值是 21M

  

2:设置 VM 参数

 
 

此时 CompressedClassSpaceSize 大小变成:60M

 
 

我们虽然没有显示设置 CompressedClassSpaceSize 大小,但是它已经变了。

这是因为 CompressedClassSpaceSize 的大小是由:MaxMetaspaceSize,InitialBootClassLoaderMetaspaceSize,CompressedClassSpaceSize 这三个参数共同影响的结果

具体就是:

min_metaspace_sz 加 CompressedClassSpaceSize 大于 MaxMetaspaceSize 的时候,CompressedClassSpaceSize 就强制被设置为(MaxMetaspaceSize - min_metaspace_sz)。

min_metaspace_sz 是 VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize 的乘积。VIRTUALSPACEMULTIPLIER 是 2,InitialBootClassLoaderMetaspaceSize 是根据-XX:InitialBootClassLoaderMetaspaceSize 的参数设置的。

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

InitialBootClassLoaderMetaspaceSize 默认 4M 也可以看到

 
 

所以加了 VM 参数之后 CompressedClassSpaceSize=68-2*4=60M。

三:GC 日志最后输出的 :Metaspace used 2425K, capacity 4498K, committed 4864K, reserved 1056768K 代表啥

在官网上:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html 有下面一段话描述这个问题。

In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata. The line beginning with class space line contains the corresponding values for the metadata for compressed class pointers.

先看一张图:

 
 

首先可以看到的是,这些used,capacity,committedreserved并不纯粹是 JVM 的概念,它和操作系统相关。

先来看committedreservedreserved是指,操作系统已经为该进程“保留”的。所谓的保留,更加接近一种记账的概念,就是操作系统承诺说一大块连续的内存已经是你这个进程的了。注意的是,这里强调的是连续的内存,并且强调的是一种名义归属。那么实际上这一大块内存有没有真实对应的物理内存呢?答案是不知道。

那么什么时候才知道呢?等进程committed的时候。当进程真的要用这个连续地址空间的时候,操作系统才会分配真正的内存。所以,这也就是意味着,这个过程会失败。

usedcapacity就是 JVM 的概念了。这两个概念非常接近 JVM 一些集合框架的概念。一些 Java 集合框架,比如某种 List 的实现,会有sizecapacity的概念。比如说ArrayList的实现里面就有capacitysize的概念。假如说我创建了一个可以存放 20 个元素的ArrayList,但是我实际上只放了 10 个元素,那么capacity就是 20,而size就是 10.这里的sizeused就是一个概念。那么“元素”则是一个个内存块"block“。

capacitycommitted的关系也可以此类比,只不过capacity反而对应到usedcommitted对应到capacity,而所谓的”元素“,就是chunk

至于class space,要记住的是,metaspace并不是全部用来放类对象的。比如说,因为每一个ClassLoader都被分配了一块内存,这块内存可能并没有被用完,于是就会有一些内存碎片;metaspace还需要放所谓静态变量。所以,class space是指实际上被用于放class的那块内存的和。

注意:

Metaspace 由一个或多个虚拟空间组成,虚拟空间的分配单元是 Chunk,其中 Chunk 使用列表进行维护。

当使用一个 classLoader 加载一个类时,过程如下:

1、 当前 classLoader 是否有对应的 Chunk 且有足够的空间;

2、 查找空闲列表中的有没有空闲的 Chunk;

3、 如果没有,就从当前虚拟空间中分配一个新的 Chunk,这个时候会把对应的内存进行 Commit,这个动作就是提交;

4、 如果当前虚拟空间不足,则预留(reserves)一个新的虚拟空间;

reserved 是 jvm 启动时根据参数和操作系统预留的内存大小。

committed 是指那些被 commit 的 Chunk 大小之和;

capacity 是指那些被实际分配的 Chunk 大小之和;

因为有 GC 的存在,有些 Chunk 的数据可能会被回收,那么这些 Chunk 属于 committe 的一部分,但不属于 capacity

另外,这些被分配的 Chunk,基本很难被 100%用完,存在碎片内存的情况,这些 Chunk 实际被使用的内存之和即 used 的大小;

所以,如何一个服务中被代理的方法特别特别多,就可能存在创建特别特别多的 classLoader 对象,一个 classLoader 对象至少需要一个 Chunk,这个 Chunk 可能只放一个 class 信息,那么就存在特别特别严重的内存碎片,继而就存在一个隐患,可能发生特别频繁的 FGC,而且是由 Metaspace 不足引起的

参考文章:

http://lovestblog.cn/blog/2016/10/29/metaspace/

https://www.imooc.com/article/305767

https://www.jianshu.com/p/cd34d6f3b5b4

https://blog.csdn.net/gx11251143/article/details/103586401/

上次编辑于:
贡献者: Andy