似乎从 Java 入门的时候,就有这样的说法来考查 Java开发者:
Java 不像 C++ 那样自己管理内存,有Java 虚拟机负责进行垃圾回收,再也没有内存泄露的问题了。 |
但是随着开发经历的增长,已经开发过应用的增多,应用内需要加载的 class 增多,经常就会遇到内存溢出(OOM)。或者更确切的说,因为加载 class 的增多导致的内存溢出是
- java.lang.OutOfMemoryError: PermGen space
此时,解决OOM的方式一般是:
1. 分析应用的代码写的是否有问题,可以通过一些工具观察应用内占用内存较多的 class 类型 (比如通过 JVisualVM 来分析Java七武器系列多情环 --多功能Profiling工具 JVisual VM,或者通过MAT来分析)
2. 修改 JVM启动参数,增大关于 Perm Gen 的配置。
在 Tomcat 这一类的 应用服务器中,由于其做为应用的容器运行,可能自身的Perm Gen 占用并不多,但需要考虑部署到容器中的应用占用。有些应用依赖了大量的第三方类库,也有一些应用会在运行时动态生成大量的 class,这些内容的加载,都容易导致 Perm Gen 的 OOM。
对于 OOM 的处理,内部会在启动时占用一小块内存,在 OOM 产生的时候释放掉来临时缓解一下,这种称为oomParachute。
除此之外,Tomcat 在 manager 应用中还提供了发现内存泄漏的功能。
图上说明写的明白,该功能主要用于分析在应用停止、重部署、解除部署时是否造成了内存泄漏。
在请求后,manager的上方信息显示区域会提示当前是否有应用造成的内存泄漏。
但需要注意的是此功能会触发一次 Full GC 的执行,代码中使用的是 System.gc(),在生产环境中如果使用需要谨慎。
那么,在什么情况下会导致所谓的应用内存泄漏呢?
我们都知道, 为了实现应用间的 class 隔离, Tomcat 对于每个应用,都会单独使用一个 WebappClassLoader,这样,多个应用间即使都使用到一个 类库的不同版本,也不会相互影响造成冲突。
但是,在这种情况下,当一个应用已经执行了停止操作,或者执行了重部署操作,此时是会生成一个新的 classLoader 来加载新部署的应用类信息。
我们知道,在 Java 中,类与类之间是存在引用关系的,类似于强引用,弱引用,幻影引用,用来在GC时将一些不需要的 class 回收掉,腾出空间。按理说之前的 classLoader 本应该被垃圾回收,但在某些时候,由于一些类之前的引用关系导致该 classLoader,以及其加载的一系列 class 文件, 都不能被标识为垃圾,此时这些 class 依然驻留在 Perm Gen,随着应用多次启停,多次重部署之后,出现了 Perm Gen 的 OOM。
一般以下类库的使用容易导致 class loader 逃过垃圾回收,产生内存泄漏:
- JDBC driver 注册
- 一些 logging 框架
- 没有移除的 ThreadLocal的使用
- 未停止的 Thread
此外,一些 Java API 的使用也容易导致此问题,例如
- javax.imageio API
- XML 解析
- RMI 使用
由于这些容易占用 classLoader,导致其不能被回收,如果这些 class 交给各个应用的类加载器进行加载,就会使得 Perm Gen 中这些类越来越多,从而产生泄漏。
为此,在 Tomcat 中引入了JreMemoryLeakPreventionListener 这个组件。实现思路是在 Tomcat 启动时,通过 System class Loader 来加载这些类。 由于类加载器的加载原理(默认父优先,而且这些系统的类,都会委托给系统类加载器进行加载),这些类不会再被 WebclassLoader 重新加载,从而减小内存泄漏的产生。
默认在 Tomcat 的配置 server.xml 中已经开启了该组件,所以这些功能你已经不知不觉中在使用。
【本文为51CTO专栏作者“侯树成”的原创稿件,转载请通过作者微信公众号『Tomcat那些事儿』获取授权】