最近,我在我的web应用程序中遇到了这个错误:

java.lang.OutOfMemoryError:永久生成空间

它是一个典型的Hibernate/JPA + IceFaces/JSF应用程序,运行在Tomcat 6和JDK 1.6上。 显然,这可能发生在重新部署应用程序几次之后。

是什么原因导致的,如何避免呢? 我该如何解决这个问题?


当前回答

在部署和取消部署一个复杂的web应用程序时,我也遇到过这个问题,我想我应该加上一个解释和我的解决方案。

当我在Apache Tomcat上部署一个应用程序时,会为该应用程序创建一个新的ClassLoader。然后使用ClassLoader加载所有应用程序的类,在取消部署时,一切都应该很好地消失。然而,在现实中,事情并没有那么简单。

在web应用程序生命周期中创建的一个或多个类持有一个静态引用,该引用在某个地方引用ClassLoader。由于引用最初是静态的,所以再多的垃圾收集也不会清理这个引用——ClassLoader和它所加载的所有类都留在这里。

在几次重新部署之后,我们遇到了OutOfMemoryError。

现在这已经成为一个相当严重的问题。我可以确保在每次重新部署后重新启动Tomcat,但这会使整个服务器停机,而不仅仅是重新部署的应用程序,这通常是不可行的。

因此,我用代码组合了一个解决方案,它可以在Apache Tomcat 6.0上工作。我没有在任何其他应用服务器上进行测试,并且必须强调,如果不进行修改,在任何其他应用服务器上都很可能无法工作。

我还想说,就我个人而言,我讨厌这段代码,如果现有代码可以更改为使用适当的关闭和清理方法,那么任何人都不应该使用这段代码作为“快速修复”。只有当您的代码所依赖的外部库(在我的例子中,它是RADIUS客户端)不提供清理其自身静态引用的方法时,才应该使用这种方法。

不管怎样,继续写代码。这应该在应用程序被取消部署时调用——例如servlet的destroy方法或(更好的方法)ServletContextListener的contextDestroyed方法。

//Get a list of all classes loaded by the current webapp classloader
WebappClassLoader classLoader = (WebappClassLoader) getClass().getClassLoader();
Field classLoaderClassesField = null;
Class clazz = WebappClassLoader.class;
while (classLoaderClassesField == null && clazz != null) {
    try {
        classLoaderClassesField = clazz.getDeclaredField("classes");
    } catch (Exception exception) {
        //do nothing
    }
    clazz = clazz.getSuperclass();
}
classLoaderClassesField.setAccessible(true);

List classes = new ArrayList((Vector)classLoaderClassesField.get(classLoader));

for (Object o : classes) {
    Class c = (Class)o;
    //Make sure you identify only the packages that are holding references to the classloader.
    //Allowing this code to clear all static references will result in all sorts
    //of horrible things (like java segfaulting).
    if (c.getName().startsWith("com.whatever")) {
        //Kill any static references within all these classes.
        for (Field f : c.getDeclaredFields()) {
            if (Modifier.isStatic(f.getModifiers())
                    && !Modifier.isFinal(f.getModifiers())
                    && !f.getType().isPrimitive()) {
                try {
                    f.setAccessible(true);
                    f.set(null, null);
                } catch (Exception exception) {
                    //Log the exception
                }
            }
        }
    }
}

classes.clear();

其他回答

对于Sun JVM,使用命令行参数-XX:MaxPermSize=128m(显然是将128替换为您需要的任何大小)。

尝试-XX:MaxPermSize=256m,如果仍然存在,尝试-XX:MaxPermSize=512m

“他们”是错误的,因为我运行的是6.0.29,即使设置了所有选项也有同样的问题。正如蒂姆·豪兰所说,这些选择只会推迟不可避免的事情。它们允许我在遇到错误之前重新部署3次,而不是每次重新部署。

我有一个Hibernate+Eclipse RCP的组合,尝试使用-XX:MaxPermSize=512m和-XX:PermSize=512m,它似乎对我有用。

目前最简单的答案是使用Java 8。

它不再专门为PermGen空间保留内存,允许PermGen内存与常规内存池混合。

请记住,您将必须删除所有非标准的-XXPermGen…JVM启动参数,如果你不想让Java 8抱怨它们什么都不做的话。