在之前的两篇中,
闲谈JVM(一):浅析JVM Heap参数配置
闲谈JVM(二):浅析新老生代参数配置
我们对JVM Heap中的两个区域,新生代与老生代进行了介绍,了解了常用的JVM参数配置与优化思路,本篇,我们将继续,对另外的一个重要区域进行深入了解——永生代(本地元空间)。
在JDK8之前,JVM中存在着方法区的概念,也可以叫做永生代(Perm),它是各个线程共享的内存区域,它用于存储已被JVM加载的类信息(Klass元数据)、常量、静态变量、即时编译器编译后的代码等数据。本篇我们采用永生代的表达说法。
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做非堆,目的应该是与Java堆区分开来。
对于永生代(Perm)的概念,事实上是HotSpot虚拟机的特定实现,HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。
对于其他虚拟机,不存在永久代的概念。原则上,如何实现方法区是属于虚拟机的实现细节,不受虚拟机规范束缚。
上面了解永生代的概念,下面我们来看一下永生代的参数配置,永生代主要的配置参数有PermSize、MaxPermSize,我们来分别看一下这两个参数的使用规则。
PermSize表示Perm内存初始值的大小,也是最小值,该参数的使用方式如下:
-XX:PermSize=10M
在Linux环境下,JDK8中该参数的默认大小即为20.79M。
需要注意的是,PermSize必须不小于1M,否则会报错。
MaxPermSize表示Perm内存最大值的大小,该参数的使用方式如下:
-XX:MaxPermSize=20M
永生代Perm的大小是在PermSize与MaxPermSize之间动态变化的,在每次进行Full GC之前,JVM会动态计算Perm空间的大小,以此来进行决定是要进行扩容操作还是缩容的操作。
当Perm的使用内存达到了MaxPermSize的值,则会触发Full GC来进行清理,有时候我们会发现老生代的使用很少,但是不断在做Full GC,可能就是Perm达到了最大阈值导致的。
当Perm的使用大小如果超过了MaxPermSize,而Full GC没来得及进行,则会抛出OOM,java.lang.OutOfMemoryError: PermGen space
的异常。
由于Perm主要存储类的相关信息,所以对于动态生成类的情况比较容易出现Perm的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。
从JDK8开始,JVM将原来存放klass元数据的永生代Perm换成了本地元空间Metaspace,Perm时期会为klass元数据分配一块内存,如果设置不够用就会抛出OOM,Metaspace的出现希望能解决这个问题,Metaspace确实可以最大限度来使用堆外的内存,但是挺遗憾,还是有一些参数会导致Metaspace抛出OOM。
对于本地元空间Metaspace,主要的配置参数有MetaspaceSize、MaxMetaspaceSize、CompressedClassSpaceSize、InitialBootClassLoaderMetaspaceSize,下面我们来分别看一下它们的使用规则。
MetaspaceSize表示Metaspace初始值的大小,该参数的使用方式如下:
-XX:MetaspaceSize=10M
看到这个参数时,我们可能会本能的对应到1.8之前Perm的PermSize的参数,但事实上,它们的规则是不一样的,MetaspaceSize并非表示Metaspace空间的实际大小,而是触发Metaspace区域的GC的阈值,达到该值后,则可能会触发垃圾收集进行类的卸载操作。
但是需要注意的是,并不是说当Metaspace的使用值超过了该阈值,就一定会触发GC操作,判断Metaspace是否真正会引起发生GC,其实看得是JVM的一个叫做
_capacity_until_GC
的变量,这个值默认会是MetaspaceSize,并且会在MetaspaceSize和MaxMetaspaceSize之间游动,所以你看到的Metaspace的使用值超过了MetaspaceSize而没有发生FGC,很有可能_capacity_until_GC
已经是在MetaspaceSize和MaxMetaspaceSize之间的一个值。
在Linux环境下,JDK8中该参数的默认大小即为20.79M。
[root@VM_0_2_centos jmvtest]# jinfo -flag MetaspaceSize 8851
-XX:MetaspaceSize=21807104
根据Oracle官方的说法,MetaspaceSize不易设置的过小,以免过早的触发Metaspace区域的GC操作:
Specify a higher value for the option
MetaspaceSize
to avoid early garbage collections induced for class metadata.
The amount of class metadata allocated for an application is application-dependent and general guidelines do not exist for the selection of
MetaspaceSize
. The default size ofMetaspaceSize
is platform-dependent and ranges from 12 MB to about 20 MB.
该参数在实际生产环境中,建议需要进行配置,可以根据项目加载类的数量适当配置,对于堆区大小4G左右,该参数可以指定为512M,仅供参考。
MaxMetaspaceSize表示Metaspace的最大值大小,该参数的使用方式如下:
-XX:MaxMetaspaceSize=100M
在Linux环境下,JDK8中该参数的默认大小为17592186044415M,可以认为是无限大。
[root@VM_0_2_centos jmvtest]# jinfo -flag MaxMetaspaceSize 10281
-XX:MaxMetaspaceSize=18446744073709547520
上面我们介绍了MetaspaceSize与MaxMetaspaceSize这两个参数,事实上,这两个参数仅仅是用来控制Metaspace空间进行GC操作的,并不是用来真正控制Metaspace的大小的,而真正控制Metaspace空间大小的参数,是CompressedClassSpaceSize、UseCompressedClassPointers、InitialBootClassLoaderMetaspaceSize。
CompressedClassSpaceSize表示Metaspace空间中存储Klass类元数据部分的空间大小,该参数的使用方式如下:
-XX:CompressedClassSpaceSize=1G
在Linux环境下,JDK8中该参数的默认大小为1G。
[root@VM_0_2_centos jmvtest]# jinfo -flag CompressedClassSpaceSize 14514
-XX:CompressedClassSpaceSize=1073741824
那么该如何理解这个参数呢,事实上,从JDK8开始,MetaSpace中会分为两部分内存空间,一部分可以叫做Klass Metaspace,一部分称为NoKlass Metaspace。
JVM启动的时候会专门分配一块内存,大小是CompressedClassSpaceSize,正常情况会类似Perm一样挨着Heap分配,这块内存专门来存类元数据的Klass部分,而CompressedClassSpaceSize则需要配合UseCompressedClassPointers参数一起使用才会真正的生效,默认情况下,UseCompressedClassPointers的值为true。
NoKlass Metaspace专门来存klass相关的其他的内容,比如method,constantPool等,这块内存是由多块内存组合起来的,所以可以认为是不连续的内存块组成的。这块内存是必须的,虽然叫做NoKlass Metaspace,但是也其实可以存klass的内容。
来看一下Oracle官方文档关于这部分的解释:
If
UseCompressedOops
is turned on andUseCompressedClassesPointers
is used, then two logically different areas of native memory are used for class metadata.
UseCompressedClassPointers
uses a 32-bit offset to represent the class pointer in a 64-bit process as doesUseCompressedOops
for Java object references. A region is allocated for these compressed class pointers (the 32-bit offsets).
The size of the region can be set with
CompressedClassSpaceSize
and is 1 gigabyte (GB) by default. The space for the compressed class pointers is reserved as space allocated bymmap
at initialization and committed as needed.
The
MaxMetaspaceSize
applies to the sum of the committed compressed class space and the space for the other class metadata.
CompressedClassSpaceSize的大小同时受到MaxMetaspaceSize的控制,当设置了MaxMetaspaceSize之后,CompressedClassSpaceSize也会相应的变化,我们来验证一下:
[root@VM_0_2_centos jmvtest]# java -XX:MaxMetaspaceSize=20M HelloWorld&
[1] 16782
[root@VM_0_2_centos jmvtest]# jinfo -flag MaxMetaspaceSize 16782
-XX:MaxMetaspaceSize=20971520
[root@VM_0_2_centos jmvtest]# jinfo -flag CompressedClassSpaceSize 16782
-XX:CompressedClassSpaceSize=12582912
上面的例子中,我们设定MaxMetaspaceSize的大小为20M,CompressedClassSpaceSize使用默认值配置,通过jinfo查看,CompressedClassSpaceSize的实际大小为12M,而并非JVM默认的1G。
UseCompressedClassPointers的默认值为true,即默认启用CompressedClassSpaceSize的配置,分配一块指定的内存空间存储类元数据Klass信息,那么当UseCompressedClassPointers为false的时候,这块指定的内存空间将不会存在,Klass信息将会与非Klass部分的空间共享Metaspace。
3InitialBootClassLoaderMetaspaceSize主要指定BootClassLoader的存储非klass部分的数据的第一个Metachunk的大小,该参数的使用方式如下:
-XX:InitialBootClassLoaderMetaspaceSize=4M
在Linux环境下,JDK8中该参数的默认大小为4M。
[root@VM_0_2_centos jmvtest]# jinfo -flag InitialBootClassLoaderMetaspaceSize 18829
-XX:InitialBootClassLoaderMetaspaceSize=4194304
我们从这个参数的名字可以猜到,它是与BootClassLoader加载器有关的,在JDK中非常多的类都是使用BootClassLoader进行加载的,每一个类加载器都会关联到一个Metaspace结构体,这个结构体会关联一系列的Metachunk来进行存储数据,当Metachunk大小不够时,则会创建新的Metachunk块,来继续存储数据,亦或者是使用空闲的Metachunk来存储数据。
而这个设计带来的问题,则会引起Metaspace空间大量的内存碎片化的问题,当Metaspace中的类加载器特别多时,每一个类加载器都会关联相应的Metachunk内存,会引起大量的内存碎片。
上面我们介绍了Metaspace内存大小相关的一系列参数,知道了MetaspaceSize并不是控制Metaspace内存的大小的参数,那么,Metaspace内存真正的大小是如何计算的呢?
事实上,Metaspace的真正大小是由Klass Metaspace + NoKlass Metaspace 两者的大小总和,Klass Metaspace是由CompressedClassSpaceSize进行指定的,而NoKlass Metaspace的默认大小则为 2 * InitialBootClassLoaderMetaspaceSize,即默认情况下,Metaspace的大小 = 1024M + 4M * 2 = 1032M。
本篇,我们介绍了JVM中的另一个重要区域,JDK1.8之前的永生代与1.8之后的本地元空间的相关参数配置,具体的参数配置,可以根据实际项目的类加载数量与类加载器的数量的情况,进行合理配置,需要注意的是,实际生产环境中,不建议使用JVM默认值。
本篇参考:
Oracle Class Metadata
JVM源码分析之Metaspace解密