GC 日志中的
Metaspace used 20580K, capacity 21180K, committed 21248K, reserved 1067008K
class space used 2594K, capacity 2752K, committed 2816K, reserved 1048576K
一、Metaspace介绍
知道jdk8之前有perm这一整块内存来存klass等信息,我们的参数里也必不可少地会配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,jvm在启动的时候会根据这些配置来分配一块连续的内存块,但是随着动态类加载的情况越来越多,这块内存我们变得不太可控,到底设置多大合适是每个开发者要考虑的问题,如果设置太小了,系统运行过程中就容易出现内存溢出,设置大了又总感觉浪费,尽管不会实质分配这么大的物理内存。基于这么一个可能的原因,于是metaspace出现了,希望内存的管理不再受到限制,也不要怎么关注元数据这块的OOM问题,虽然到目前来看,也并没有完美地解决这个问题。
或许从JVM代码里也能看出一些端倪来,比如MaxMetaspaceSize默认值很大,CompressedClassSpaceSize默认也有1G,从这些参数我们能猜到metaspace的作者不希望出现它相关的OOM问题。
1.1 Metaspace的空间组成
metaspace其实由两大部分组成
- Klass Metaspace (类的信息)
- NoKlass Metaspace (运行时常量池 )
1.1.1 Klass Metaspace (类的信息)
Klass Metaspace就是用来存klass的,klass是我们熟知的class文件在jvm里的运行时数据结构,不过有点要提的是我们看到的类似A.class其实是存在heap里的,是java.lang.Class的一个对象实例。这块内存是紧接着Heap的,和我们之前的perm一样,这块内存大小可通过-XX:CompressedClassSpaceSize参数来控制,这个参数前面提到了默认是1G,但是这块内存也可以没有,假如没有开启压缩指针就不会有这块内存,这种情况下klass都会存在NoKlass Metaspace里,另外如果我们把-Xmx设置大于32G的话,其实也是没有这块内存的,因为会这么大内存会关闭压缩指针开关。还有就是这块内存最多只会存在一块。
1.1.2 NoKlass Metaspace (运行时常量池 )
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的大小了。我们先来做个小测试。
可以使用 jinfo -flag CompressedClassSpaceSize 查看默认的 Klass Metaspace 类加载空间大小为1G
[pdm@localhost dumplog]$ jps -l
3218 xxxx.jar
[pdm@localhost dumplog]$ jinfo -flag CompressedClassSpaceSize 13933
-XX:CompressedClassSpaceSize=1073741824
1073741824换算后正好1G
1.3 设置JVM参数 对 Klass Metaspace (类的信息)的影响
设置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
Metaspace used 20580K, capacity 21180K, committed 21248K, reserved 1067008K
class space used 2594K, capacity 2752K, committed 2816K, reserved 1048576K
首先可以看到的是,这些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“。
capacity和committed的关系也可以此类比,只不过capacity反而对应到used,committed对应到capacity,而所谓的”元素“,就是chunk。
至于class space,要记住的是,metaspace并不是全部用来放类对象的。比如说,因为每一个ClassLoader都被分配了一块内存,这块内存可能并没有被用完,于是就会有一些内存碎片;metaspace还需要放所谓静态变量。所以,class space是指实际上被用于放class的那块内存的和。
至于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不足引起的
三、 Metaspace 配置的参数
Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。当然你也可以通过以下的几个参数对Metaspace进行控制:
-XX:MetaspaceSize=N
这个参数是初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控Metaspace的大小,可能增加上线也可能降低。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约20.8M)。
-XX:MaxMetaspaceSize=N
这个参数用于限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。
-XX:MinMetaspaceFreeRatio=N
当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增长Metaspace的大小。在本机该参数的默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。
-XX:MaxMetasaceFreeRatio=N
当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。在本机该参数的默认值为70,也就是70%。
-XX:MaxMetaspaceExpansion=N
Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。
-XX:MinMetaspaceExpansion=N
Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。
以前只认为,Metaspace区是保存在本地内存中,是没有上限的,经查阅资料才发现,原来JDK8中,XX:MaxMetaspaceSize确实是没有上限的,最大容量与机器的内存有关;但是XX:MetaspaceSize是有一个默认值的:21M