jvm 内存溢出 - 方法区及运行时常量池溢出

Java 永久代是非堆内存的组成部分,用来存放类名、访问修饰符、常量池、字段描述、方法描述等,因运行时常量池是方法区的一部分,所以这里也包含运行时常量池。我们可以通过 jvm 参数 -XX:PermSize=10M -XX:MaxPermSize=10M 来指定该区域的内存大小,-XX:PermSize 默认为物理内存的 1/64 ,-XX:MaxPermSize 默认为物理内存的 1/4 。String.intern() 方法是一个 Native 方法,它的作用是:如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象;否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用。在 JDK 1.6 及之前的版本中,由于常量池分配在永久代内,我们可以通过 -XX:PermSize-XX:MaxPermSize 限制方法区大小,从而间接限制其中常量池的容量,通过运行 java -XX:PermSize=8M -XX:MaxPermSize=8M RuntimeConstantPoolOom 下面的代码我们可以模仿一个运行时常量池内存溢出的情况:

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

public class RuntimeConstantPoolOom {
  public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    int i = 0;
    while (true) {
      list.add(String.valueOf(i++).intern());
    }
  }
}

运行结果如下

[root@9683817ada51 oom]# ../jdk1.6.0_45/bin/java -XX:PermSize=8m -XX:MaxPermSize=8m RuntimeConstantPoolOom
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at RuntimeConstantPoolOom.main(RuntimeConstantPoolOom.java:9)

还有一种情况就是我们可以通过不停的加载class来模拟方法区内存溢出,《深入理解java虚拟机》中借助 CGLIB 这类字节码技术模拟了这个异常,我们这里使用不同的 classloader 来实现(同一个类在不同的 classloader 中是不同的),代码如下

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;

public class MethodAreaOom {
  public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
    Set<Class<?>> classes = new HashSet<Class<?>>();
    URL url = new File("").toURI().toURL();
    URL[] urls = new URL[]{url};
    while (true) {
      ClassLoader loader = new URLClassLoader(urls);
      Class<?> loadClass = loader.loadClass(Object.class.getName());
      classes.add(loadClass);
    }
  }
}

运行结果如下:

[root@9683817ada51 oom]# ../jdk1.6.0_45/bin/java -XX:PermSize=2m -XX:MaxPermSize=2m MethodAreaOom
Error occurred during initialization of VM
java.lang.OutOfMemoryError: PermGen space
    at sun.net.www.ParseUtil.<clinit>(ParseUtil.java:31)
    at sun.misc.Launcher.getFileURL(Launcher.java:476)
    at sun.misc.Launcher$ExtClassLoader.getExtURLs(Launcher.java:187)
    at sun.misc.Launcher$ExtClassLoader.<init>(Launcher.java:158)
    at sun.misc.Launcher$ExtClassLoader$1.run(Launcher.java:142)
    at java.security.AccessController.doPrivileged(Native Method)
    at sun.misc.Launcher$ExtClassLoader.getExtClassLoader(Launcher.java:135)
    at sun.misc.Launcher.<init>(Launcher.java:55)
    at sun.misc.Launcher.<clinit>(Launcher.java:43)
    at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1337)
    at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1319)

在 jdk1.8 上运行上面的代码将不会出现异常,因为 jdk1.8 已结去掉了永久代,当然 -XX:PermSize=2m -XX:MaxPermSize=2m 也将被忽略,如下

[root@9683817ada51 oom]# java -XX:PermSize=2m -XX:MaxPermSize=2m MethodAreaOom
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=2m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=2m; support was removed in 8.0

jdk1.8 使用元空间( Metaspace )替代了永久代( PermSize ),因此我们可以在 1.8 中指定 Metaspace 的大小模拟上述两种情况

[root@9683817ada51 oom]# java -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m RuntimeConstantPoolOom
Error occurred during initialization of VM
java.lang.OutOfMemoryError: Metaspace
    <<no stack trace available>>
[root@9683817ada51 oom]# java -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m MethodAreaOom
Error occurred during initialization of VM
java.lang.OutOfMemoryError: Metaspace
    <<no stack trace available>>

说明

文章摘自《深入理解Java虚拟机》第二版 周志明著,仅作为学习记录,书籍中用到的案例代码及描述有部分修改,但未改变原意。