引言: JVM中的内存区域一般分为3个部分: 年轻代、年老代和永久代;永久代在JDK 7中逐渐变化,到JDK 8之后完全消失,合并到了Native堆中。本文将逐个分析其中的使用和状况。
1. 环境说明
windows 7, JDK 1.7.0_79/JDK 1.8.0_45. STS
2. 何为永久区?
主要是JVM在运行过程中,存放Class的静态信息,Main方法信息,常量信息,静态方法和变量信息,共享变量等信息。一般很少被JVM进行回收。一般的动态替换Class的行为都是在这个区域来进行的。
3. JDK 7下的永久区参数设置
参数设置示例: -XX:PermSize=5M -XX:MaxPermSize=7M
说明: PermSize为永久区大小, MaxPermSize为最大的永久区大小。
代码示例:
JVM参数: -XX:PermSize=5M -XX:MaxPermSize=7M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\oom.dump
import java.lang.reflect.Method;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Dispatcher;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class OOMTest {
public static void main(String[] args) {
System.out.println("Let us do it now.....");
for(int i=0;i<100000;i++){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BaseFlyer.class);
enhancer.setCallbackTypes(new Class[] {
Dispatcher.class, MethodInterceptor.class });
enhancer.setCallbackFilter(new CallbackFilter() {
public int accept(Method method) {
return 1;
}
});
Class clazz = enhancer.createClass();
System.out.println("Time:" + System.currentTimeMillis());
}
}
}
输出结果:
Time:1470203973444
Time:1470203973447
Time:1470203973449
Time:1470203973452
Time:1470203973456
Time:1470203973459
java.lang.OutOfMemoryError: PermGen space
Dumping heap to d:\oom.dump ...
Unable to create d:\oom.dump: File exists
Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
注意这里的关键词为:java.lang.OutOfMemoryError: PermGen space, 永久区溢出。
基于jvisualvm来打开dump文件,可以发现defineClass的时候发生的内存溢出,另外出现了大量的BaseFlyer子类的动态类,这个导致了Perm区域的溢出。
在defineClass()之时,报出溢出错误信息:
在JDK 7中开始进行移除永久代的努力,下面列出了JDK7中从永久带移除的东西:
- 符号引用被移到了native堆
- 池化string对象被移到了java堆
- Class对象、静态变量被移到了java堆
4. JDK 8中的永久区设置
JDK8中已经完全移除了永久带。这项工作是在这个bug:https://bugs.openjdk.java.net/browse/JDK-6964458推动下完成的。JDK8中,PermSize和MaxPermSize参数也一并移除了。
在移除了Perm区域之后,JDK 8中使用MetaSpace来替代,这些空间都直接在堆上来进行分配。 在JDK8中,类的元数据存放在native堆中,这个空间被叫做:元数据区。JDK8中给元数据区添加了一些新的参数。
- -XX:MetaspaceSize=<NNN> <NNN>是分配给类元数据区(以字节计)的初始大小(初始高水位),超过会导致垃圾收集器卸载类。这个数量是一个估计值。当第一次到达高水位的时候,下一个高水位是由垃圾收集器来管理的。
- -XX:MaxMetaspaceSize=<NNN> <NNN>是分配给类元数据区的最大值(以字节计)。这个参数可以用来限制分配给类元数据区的大小。这个值也是个估计值。默认无上限。
- -XX:MinMetaspaceFreeRatio=<NNN>,<NNN>是一次GC以后,为了避免增加元数据区(高水位)的大小,空闲的类元数据区的容量的最小比例,不够就会导致垃圾回收。
- -XX:MaxMetaspaceFreeRatio=<NNN>,<NNN>是一次GC以后,为了避免减少元数据区(高水位)的大小,空闲的类元数据区的容量的最大比例,超过就会导致垃圾回收。
默认情况下,类元数据的分配仅受限于可用的本地内存。我们可以使用新的MaxMetaspaceSize参数限定类元数据可用的本地内存的数量。它类似于MaxPermSize。当类元数据区使用量到达MetaspaceSize(32位机客户端模式12M,32位服务器模式16M,64位机会更大)的时候,会触发垃圾回收,然后回收掉无用的类加载器和class对象。
MetaspaceSize的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
代码示例:
JVM参数设置: -XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=7M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\oom.dump
cglib依赖包:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
代码内容:
import java.lang.reflect.Method;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Dispatcher;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class OOMTest {
public static void main(String[] args) {
System.out.println("Let us do it now.....");
for(int i=0;i<100000;i++){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(BaseFlyer.class);
enhancer.setCallbackTypes(new Class[] {
Dispatcher.class, MethodInterceptor.class });
enhancer.setCallbackFilter(new CallbackFilter() {
public int accept(Method method) {
return 1;
}
});
Class clazz = enhancer.createClass();
System.out.println("Time:" + System.currentTimeMillis());
}
}
}
运行结果:
Time:1470217316712
Time:1470217316716
Time:1470217316718
Time:1470217316721
Time:1470217316724
Time:1470217316727
Time:1470217316730
java.lang.OutOfMemoryError: Metaspace
Dumping heap to d:\oom.dump ...
Heap dump file created [2577942 bytes in 0.022 secs]
Exception in thread "main" java.lang.IllegalStateException: Unable to load cache item
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:79)
at net.sf.cglib.core.internal.LoadingCache.get(LoadingCache.java:34)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:116)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.createClass(Enhancer.java:337)
at org.homework.test.jvm.jvmopt.OOMTest.main(OOMTest.java:25)
Caused by: java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:93)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData$3.apply(AbstractClassGenerator.java:91)
at net.sf.cglib.core.internal.LoadingCache$2.call(LoadingCache.java:54)
at java.util.concurrent.FutureTask.run(Unknown Source)
at net.sf.cglib.core.internal.LoadingCache.createEntry(LoadingCache.java:61)
... 6 more
分析错误日志,发现Metaspace是核心关键词,如果发现类似的关键词,即可确定是Metaspace的设置问题。
基于jvisualvm来进行dump文件分析的结果与JDK 7中的结果类似,这里不再重复赘述,各位可以自行研究分析。
4. G1垃圾回收算法
G1是在JDK 7中推出的新GC算法,相比之前的JVM内存模型,这是一个巨大的飞跃,慢慢打破了不同代系之间的阻隔。 G1和永久区的逐步消失是密切耦合的。
在G1中,堆被划分成 许多个连续的区域(region)。每个区域大小相等,在1M~32M之间。JVM最多支持2000个区域,可推算G1能支持的最大内存为2000*32M=62.5G。区域(region)的大小在JVM初始化的时候决定,也可以用-XX:G1HeapReginSize设置。
在G1中没有物理上的Yong(Eden/Survivor)/Old Generation,它们是逻辑的,使用一些非连续的区域(Region)组成的。
G1虽然保留了CMS关于代的概念,但是代已经不是物理上连续区域,而是一个逻辑的概念。在标记过程中,每个区域的对象活性都被计算,在回收时候,就可以根据用户设置的停顿时间,选择活性较低的区域收集,这样既能保证垃圾回收,又能保证停顿时间,而且也不会降低太多的吞吐量。Remark阶段新算法的运用,以及收集过程中的压缩,都弥补了CMS不足。引用Oracle官网的一句话:“G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS)”。
这里先简要提一下G1的垃圾回收算法,稍后我还会专门描述G1的回收算法。
5. 总结
在这里我们分别在JDk 7和JDK 8针对永久区进行了分析,这里需要着重强调的是两者之间是无法彼此兼容的设置,各自自行设置调整。