JVM中Metaspace理解

JVM参数MetaspaceSize的误解

以笔者测试环境上某个服务为例,配置了-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m,通过jstat -gcutil pid查看M的值为98.32,即Meta区使用率达到了98.32%

然后,再通过jstat -gc 4210 2s 3命名查看,结果如下图所示,计算MU/MC即Meta区的使用率确实达到了98.32%,但是MC,即Metaspace Capacity只有55296K,并不是参数MetaspaceSize指定的256M

那么-XX:MetaspaceSize=256m的含义到底是什么呢?其实,这个JVM参数是指Metaspace扩容时触发FGC的初始化阈值,也是最小的阈值。这里有几个要点需要明确:

  • 如果没有配置-XX:MetaspaceSize,那么触发FGC的阈值就是21807104(20.8M),可以通过jinfo -flag MetaspaceSize pid得到这个值
  • 如果配置了-XX:MetaspaceSize,那么触发FGC的阈值就是配置的值
  • Metaspace由于使用不断扩容到-XX:MetaspaceSize参数指定的量,就会发生FGC;且之后每次Metaspace扩容都可能会发生FGC(至于什么时候会,比较复杂,跟几个参数有关)
  • 如果old区配置CMS垃圾回收,那么扩容引起的FGC也会使用CMS算法进行回收
  • 如果MaxMetaspaceSize设置过小,可能会导致频繁发生FGC,甚至OOM

任何一个JVM参数的默认中可以通过java -XX:+PrintFlagsFinal -version | grep JVMParamName获取,例如java -XX:+PrintFlagsFinal -version |grep MetaspaceSize

JDK7的PermSize

JDK8+移除了Perm,引入了Metaspace,它们两者的区别是什么呢?Metaspace上面已经总结了,无论-XX:MetaspaceSize-XX:MaxMetaspaceSize两个参数如何设置,随着类加载越来越多不断扩容调整,直到MetaspaceSize(如果没有配置就是默认20.8m)触发FGC,上限是-XX:MaxMetaspaceSize,默认是无穷大。而Perm的话,我们通过配置-XX:PermSize以及-XX:MaxPermSize来控制这块内存的大小,JVM在启动的时候会根据-XX:PermSize初始化分配一块连续的内存块,这样的话,如果-XX:PermSize设置过大,就是一种赤裸裸的浪费。很明显,Metaspace比Perm好多了。

PermGen与Metaspace

PermGen

绝大部分的都见过“java.lang.OutOfMemoryError:PermGen space”这个异常,这里的PermGen space其实指的就是方法区。不过方法区和PermGen space又有着本质的区别。前者是JVM的规范,而后者是JVM规范的一种实现,并且只有Hotspot才有PermGen space,对于其他的虚拟机,并没有这个区域。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的应用场景就是,在JSP页面比较多的情况,容易出现永久代的内存溢出。下面我们举例说明

1
2
3
4
package com.paddx.test.memory;

public class Test {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.paddx.test.memory;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

public class PermGenOomMock{
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.paddx.test.memory.Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果如下:

Metaspace

JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.paddx.test.memory;

import java.util.ArrayList;
import java.util.List;

public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}

这段程序以2的指数级不断的生成新的字符串,这样可以比较快速的消耗内存。我们通过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:

JDK 1.6 的运行结果

JDK 1.7的运行结果

JDK 1.8的运行结果

从上述结果可以看出,JDK1.6下,会出现PermGen Space的内存溢出,而在JDK1.7和JDK1.8中,会出现堆内存溢出,并且JDK1.8中PermSizeMaxPermGen已经无效。因此,可以大致验证JDK1.7和1.8将字符串常量由永久代转移到堆中,并且JDK1.8中已经不存在永久代。

元空间的本质和永久代相似,都是JVM规范中对方法区的实现。**不过元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。**因此,默认情况下,元空间的大小仅受本地内存的限制,但可以通过以下参数来指定元空间的大小

  • XX:MetaspaceSize:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
  • XX:MaxMetaspaceSize:最大空间,默认是没有限制的

除了指定上面两个参数,还有两个与GC相关的属性

  • XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

总结

通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:

  • 字符串存在永久代中,容易出现性能问题和内存溢出
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏
  • Oracle 可能会将HotSpot 与 JRockit 合二为一
Author: Toyan
Link: https://toyan.top/jvm-metaspaceSize/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
支付宝打赏
微信打赏