Tomcat常见内存错误的处理

来源:互联网 发布:seo标签优化 编辑:程序博客网 时间:2024/05/13 14:51

如何处理内存溢出错误

  这种错误在开发阶段相当常见,甚至产品阶段也是如此。这种错误相当令人厌烦,因为其不显示任何栈跟踪信息,这是因为栈跟踪信息对这个错误没有帮助。代码发生内存溢出错误,大部分原因可能是代码的问题,少数情况可能不是代码本身的问题。虽然这些错误令人迷惑令人把原因归咎于Tomcat,但是这些发生在web程序中的错误大多数有其自身的原因。这些错误经常发生在以独立应用程序运行非常正常的编程模式和技术相当完美的情况下,但是在一个受管理的servlet容器中例如Tomcat就不正确。本文将列出一些常见的错误,也许一些人已经遇到过此类问题,也许你想避免这些问题而检查他们的web程序而纠正它们。
一般的原则
首先要理解这些模式的基础。理解了这些,开发者甚至能够解决在此处未曾列出的问题。一个内存溢出问题可能由于以下原因:
    ●一个servlet尝试加载一个几GB的文件到内存,这会“杀死”服务器,这种问题应该看作我们程序的BUG。
    ●如果你为了让servlet加载更多的数据而增加了堆(heap)内存,那可能使栈(stack)可用内存减少而出现错误。在一些系统上每个线程需要消耗2M栈内存,而且在某些操作系统例如Debian无法通过-Xss参数来减少每个线程占用的栈内存。在32位web程序中不要使用大于1GB的堆(heap)内存。
    ●过深的递归算法也会导致内存溢出问题。对于这种情况,需要通过-Xss参数调整一个线程可用的栈内存,或者调整算法减少递归深度,或者减少每次调用的本地数据的大小。
    ●一个web程序使用了太多的库或者一个服务器下运行了太多的web应用程序,这会耗尽JVM持久内存(不会被回收和释放)的空间。持久内存是JVM用来存储类和方法数据的地方。这种情况你可以增加这个内存,在Sun的JVM你可以使用-XX:MaxPermSize来调整,其默认是64M。
    ●对类的硬参考能够在ClassLoader被丢弃的时候阻止垃圾回收器回收它们。这个发生在JSP重复编译或者重新加载一个应用程序。这个操作如果经常发生就容易出现这个问题,它仅仅是个时间的问题,直到JVM持久内存被耗尽而抛出内存溢出错误。
  对于最后一个情况,直接的事实就是webapp是运行在被管理的环境中,在这种情况下改变的代码是在不需要停止服务器的情况下被提交,也就是说即使在独立应用程序中完全正常的代码也会出问题,避免此问题的方法就是减少重新加载web程序的次数或者完全重新启动servlet容器。
线程
必须没有其它的线程运行在servlet中。否则其本地变量、类和全部类装载器被硬参考。
DriverManager
如果你在你自己的classloader或者servlet中加载一个java.sql.Driver,在webapp卸载之前需要把其移出。每一个在DriverManager中注册的驱动被系统类装载器装载并被引用成本地驱动。
Enumeration<Driver> drivers = DriverManager.getDrivers();
ArrayList<Driver> driversToUnload=new ArrayList<Driver>();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
if (driver.getClass().getClassLoader().equals(getClass().getClassLoader())) {
driversToUnload.add(driver);
}
}
for (Driver driver : driversToUnload) {
    DriverManager.deregisterDriver(driver);
}
单一(Singleton)模式
这是一个在java程序中非常有用的模式。它在独立应用程序中运行良好,看起来如下:
public class MyClass {
  private static final MyClass instance = new MyClass();
  public static MyClass getInstance() {
    return instance;
  }
  private MyClass() { }
}
这个模式的问题是它自己创建了一个对自己实例的硬参考。如果这个实例不被释放,类就不能被卸载,最终导致服务器不能回收整个webapp类装载器的空间。下面的一些解决办法为了服务器的完整性将牺牲原有的良好的设计模式,这个需要设计或者开发者自己决定是否值得这样做。
确定问题
在重新规划自己的程序之前,请确认单一模式是否是问题的真正原因。我的单一默认虽然最终明确移出了,但是Tomcat依旧会崩溃。
   1.覆盖单一模式类的finalize方法,放置一个System.out.println()来输出提示信息。
   2.在构造函数里调用几次System.gc(),这是因为垃圾回收器是惰性的。
   3.重复加载一定次数自己的应用程序
   4.可能你会看到旧的singleton被移出,这是类装载器被垃圾回收器回收才出现的情况。
   5.重复几次加载web程序后内存溢出发生,即使一个或者两个singletons在内存中。
方案1: 把类移动到另一个类装载器中
这个方案适合在webapps之间共享这个类的情况,或者服务器仅包含一个webapp。那就是我们需要在同一个服务器中使用同一个实例跨越几个webapps,或者不需要担心这个事情。在这个情况下,类需要在一个共享类装载器上布置。这意味着这个类必须放在共享的 lib或者共享的classes目录下。这种方法,类被一个父类装载器装载,而不是webapp类装载器本身,因此没有资源需要卸载在webapp重新装载时。这个方案并不总适合你的代码和设计。特别需要注意避免单一模式类参考通过webapp类装载器装载的类,因为这样的参考将会阻止类装载器的销毁。Servlet上下文监听器(ServletContextListener)可以用来在上下文销毁的时候去除这个参考。
方案2: 使用commons-discovery
如果你有一个为每一个webapp使用单一模式实例,你应该使用commons-discovery。这个库提供了一个名字为DiscoverSingleton的类用来在你的webapp中实现单一模式。为了使用它,作为单一模式类需要实现方法使用的一个接口(SPI)。下面的代码是使用这个库的例子:
  MyClass instance = DiscoverSingleton.find(MyClass.class, MyClassImpl.class.getName());
对于这个库工作正常它是非常重要的,不需亚对返回的实例保持静态的参考。
使用这个语法,你能得到如下好处:
    ●任一类都可用作单一模式类,只要其实现了一个SPI接口。
    ●你的单一模式类已经被转换成一个可替换的组件,因此可以在你需要的时候插入一个不同的实现。
但是仅这些不不是本方案的全部,重要的益处是其DiscoverSingleton.release()方法,它会释放当前webapp类装载器参考的单一模式类。这个方法应该放在ServletContextListener中的contextDestroyed()方法中。
简单的示例如下:
public class SingletonReleaser implements ServletContextListener {
  public contextInitialized(ServletContextEvent event) { }
  public contextDestroyed(ServletContextEvent event) {
    DiscoverSingleton.release();
  }
}
当然需要在在其它监听器使用一个单一模式类之前在web.xml中注册这个监听器。
方案3: 使用ServletContext属性
这个方案在ServletContext实例可用的时候工作的很好,作为一个本地变量或者一个参数。它比使用commons-discovery更有效率,但是不利之处是依赖于web层(ServletContext class)。我发现在一些情况下,它也是合理的方式。
有很多方式来实现它,在此我仅例示我使用良好的代码:
    ●创建如下ServletContextListener:
public class SingletonFactory implements ServletContextListener {
  public static final String MY_CLASS = "...";
  /**
   * @see ServletContextListener#contextInitialized(ServletContextEvent)
   */
  public void contextInitialized(ServletContextEvent event) {
    ServletContext ctx = event.getServletContext();
    ctx.setAttribute(MY_CLASS, new MyClass());
  }
  /**
   * @see ServletContextListener#contextDestroyed(ServletContextEvent)
   */
  public void contextDestroyed(ServletContextEvent event) {
    ctx.setAttribute(MY_CLASS, null);
  }
  /**
   * Optional method for getting the MyClass singleton instance.
   */
  public static MyClass getMyClassInstance(ServletContext ctx) {
    return (MyClass)ctx.getAttribute(MY_CLASS);
  }
}
    ●在web.xml中注册监听器
    ●通过如下方式替换对MyClass.getInstance()的调用:
  MyClass instance = (MyClass)ctx.getAttribute(SingletonFactory.MY_CLASS);
  /* or, if implemented:
  MyClass instance = SingletonFactory.getMyClassInstance(ctx);
  */