一:Metaspace介绍
我们都知道jdk8之前有perm这一整块内存来存klass等信息,我们的参数里也必不可少地会配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,jvm在启动的时候会根据这些配置来分配一块连续的内存块,但是随着动态类加载的情况越来越多,这块内存我们变得不太可控,到底设置多大合适是每个开发者要考虑的问题,如果设置太小了,系统运行过程中就容易出现内存溢出,设置大了又总感觉浪费,尽管不会实质分配这么大的物理内存。基于这么一个可能的原因,于是metaspace出现了,希望内存的管理不再受到限制,也不要怎么关注元数据这块的OOM问题,虽然到目前来看,也并没有完美地解决这个问题。
或许从JVM代码里也能看出一些端倪来,比如MaxMetaspaceSize默认值很大,CompressedClassSpaceSize默认也有1G,从这些参数我们能猜到metaspace的作者不希望出现它相关的OOM问题。
二:Metaspace的空间组成
metaspace其实由两大部分组成
Klass Metaspace
NoKlass Metaspace
//加入Java开发交流君样:756584822一起吹水聊天
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的参数设置的。
//加入Java开发交流君样:756584822一起吹水聊天
-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,committed和reserved并不纯粹是JVM的概念,它和操作系统相关。
先来看committed和reserved。reserved是指,操作系统已经为该进程“保留”的。所谓的保留,更加接近一种记账的概念,就是操作系统承诺说一大块连续的内存已经是你这个进程的了。注意的是,这里强调的是连续的内存,并且强调的是一种名义归属。那么实际上这一大块内存有没有真实对应的物理内存呢?答案是不知道。
那么什么时候才知道呢?等进程committed的时候。当进程真的要用这个连续地址空间的时候,操作系统才会分配真正的内存。所以,这也就是意味着,这个过程会失败。
used和capacity就是JVM的概念了。这两个概念非常接近JVM一些集合框架的概念。一些Java集合框架,比如某种List的实现,会有size和capacity的概念。比如说ArrayList的实现里面就有capacity和size的概念。假如说我创建了一个可以存放20个元素的ArrayList,但是我实际上只放了10个元素,那么capacity就是20,而size就是10.这里的size和used就是一个概念。那么“元素”则是一个个内存块"block“。
//加入Java开发交流君样:756584822一起吹水聊天
capacity和committed的关系也可以此类比,只不过capacity反而对应到used,committed对应到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大小之和;
//加入Java开发交流君样:756584822一起吹水聊天
capacity是指那些被实际分配的Chunk大小之和;
因为有GC的存在,有些Chunk的数据可能会被回收,那么这些Chunk属于committe的一部分,但不属于capacity
另外,这些被分配的Chunk,基本很难被100%用完,存在碎片内存的情况,这些Chunk实际被使用的内存之和即used的大小;
所以,如何一个服务中被代理的方法特别特别多,就可能存在创建特别特别多的classLoader对象,一个classLoader对象至少需要一个Chunk,这个Chunk可能只放一个class信息,那么就存在特别特别严重的内存碎片,继而就存在一个隐患,可能发生特别频繁的FGC,而且是由Metaspace不足引起的