简单记录一次远古版本dubbo发生的PermGen space异常
环境介绍: dubbo的版本是比较旧的版本, 肯定是小于2.5的, jdk版本是1.7, 默认使用的是HotSpot虚拟机
前提说明: dubbo版本应该就是最原始的2.x的版本, 由于在这个基础上公司还经过了自己的自定义封装, 所以升级的话肯定是没戏的, 其次, 也是由于某些模块很少使用到, 所以一直没暴露出来问题
生产环境oom现象: 生产上刚启动一段时间内是可以正常使用的, 几天之后服务就挂了, 必须重启之后才能重新对外提供服务, 通过日志可以发现报错:OutOfMemoryError PermGen space, 这种情况用脚都能猜出来是内存泄露, 也是jvm中永久代内存有些一直没有被回收, 而且还不断的往永久代中新增东西
网上的解决方案: 使用-XX:MaxPermSize调整一下永久代的最大空间! 尼玛, 这就很离谱, 这不是治标不治本么, 这种方法顶多就是你的系统一个星期oom变为了两三个星期再oom一次了, 如果在oom之前又有新的项目上线重启一下服务, 都可以苟活一段时间了
下面就简单介绍一下我这次出现的问题吧
1. 啥是永久代
首先要知道, 方法区是jvm规范, 而永久代是方法区的实现, 他们就类似于接口和实现类的关系, 所以下面我把方法区和永久代看作是等价的
在我们java程序要启动的时候, 就需要加载很多的类, 可以把每个类看作是class文件, 通过类加载器加载进了永久代, 我们就把永久代中数据看作是类的元数据, 其中包含了常量池, 字段, 方法等信息
再深入一点, 我们知道java之中还有一个Class对象, 这个Class对象就是根据永久代中的元数据生成的, 这放在java堆中;
实例化对象的时候有两种方式:
方式一: 根据元数据来进行实例化的, 下图所示, Class对象对于同一个类加载器加载的, 只能有一个, 和方法区中元数据一一对应,而实例可以有多个
方式二: 使用反射根据Class对象进行实例化对象
我们常用的获取Class对象有三种方式:
(1)Class.forName(“ClassName”):通过类的元数据中的Class对象引用获得Class对象
(2)object.getClass():通过实例对象中保存的对类的元数据的引用获取类的元数据,再通过元数据中对Class对象的引用获取Class对象
(3)ClassName.class:通过类的元数据中的Class对象引用获得class对象
2. 永久代有大小么?
从上面的图中可以看到永久代其实也是属于堆中一部分, 可以在启动的时候设置永久代的容量和最大的容量, 例如: -XX:PermSize=64m,-XX:MaxPermSize=128m
那么问题来了, 永久代如果设置太小了怎么办? 结果就是java程序启动的时候, 都会报永久代oom, 或者项目启动了之后需要动态加载第三方jar包的时候, 发生oom
永久代进行gc的条件:
(1) 该类的实例都被回收
(2) 加载该类的classLoader已经被回收
(3) 该类不能通过反射访问到其方法,而且该类的java.lang.class没有被引用 当满足这3个条件时,是可以回收,但回不回收还得看jvm
如果你的服务器上oom了, 第一反应不是重启, 而是最快时间拷贝一份堆栈快照, 可以使用jmap -dump:live, format=b,file=dumpxxx.hprof pid, 其中dump表示要导出一份堆栈信息文件, live表示要把活的对象导出, format=b, 文件格式是二进制; file表示要导出的文件保存的全路径; pid表示进程id
3. OOM具体原因和解决方案
这里就不放堆栈信息了, 公司内部的东西, 反正就是MAT工具进行一顿猛分析, 发现里面那种ClassLoader$ApplicantClassLoader比较多, 推测加载的元数据信息到永久代中很多, 然后其他的信息也看不出来啥, 水平比较菜o(╥﹏╥)o
然后我尝试在本地搭建了环境, 试试能不能复现出来, 调整了一下堆栈参数, 使用jmeter压测了几个小时之后, 还真的复现了
因为以前是没有出现过的这个问题, 先检查代码, 都是业务代码, 没有涉及到cglib动态代理这种的使用!
肯定是这次上线的版本新功能有哪里涉及到了, 经过排查, 这次新的东西就是多使用到了一个框架层次的工具类, 是对缓存的抽象, 通过生成缓存的代理类, 去操作redis, 猜测就是这个类的影响
最简单直接的排查方式就是自己手动写一个redis的工具类, 然后统统替换掉那个工具类, 然后压测一段时间, 就没有这个问题了
通过手动debug的方式, 最后到了一个Proxy的工具类中, 就是这里涉及到了cglib动态代理, 不断的拼接java类字符串, 然后加载到方法区中, 生成class对象, 然后通过class对象反射生成实例, 可能就是这里的原因, 由于这个类看的不是很懂, 我就去github上的dubbo的issue搜了一下oom,看有没有相类似的问题, 还真的被我搜出来了,
继续点进去发现了一些很有意思的东西
(1) proxy instance cause a PermSpace OOM #6742
因为 org.apache.dubbo.common.bytecode.Proxy 中使用的Proxy对象缓存导致。PROXY_CACHE_MAP 缓存的Proxy实例,使WeakReference,full GC 后会释放该Proxy实例再次申请对象实例时,Proxy会重新创建Proxy的Class对象,最终导致PermGen space内存溢出。应该修改为缓存该Proxy Class,而不是 Proxy 对象实例。
(2) commit记录
最大的改变其实就是增加了这么一个Map, 用于缓存生成代理类的Class对象, 每次先去PROXY_CACHE_MAP看看实例对象有没有, 没有的话, 再去PROXY_CLASS_MAP中找到对应的Class对象, 如果还是没有才会去拼接java类, 然后加载到永久代中, 然后再缓存Class对象, 以后就不需要再加载了
而由于我这里dubbo项目比较老, 每次都要去加载类的信息到永久代中, 时间久了, 永久代就挂了
既然找到了问题所在, 改的话, 也就简单了, 直接打开dubbo的源码抄就好了, 毕竟我可是ctrl+c ctrl+v的高手, 这点我还是蛮有自信的
这个问题只有在dubbo2.7的版本才被修复, 可以打开2.7.x版本的org.apache.dubbo.common.bytecode.Proxy类的代码, 抄一抄就解决了
4. 总结
一个oom的问题涉及的东西是真的多, 首先涉及到要很了解java类的加载机制, 以及jvm内存结构, cglib动态代理, 在服务器端使用jmap导出堆栈信息, MAT内存分析工具的使用, jmeter性能压测, dubbo源码阅读以及调试, github查找相关问题, 真尼玛的麻烦o(╥﹏╥)o
而且这次真的是体会到了一活跃的开源社区的强大之处, 要是一个使用的人都比较少的框架, 都没几个人, 靠自己很难发现问题的所在之处, 并不是每个人都有修改源码的能力的, 害
再一次提醒我们要多提高技术, 有时间就关注开源社区的一些问题以及最新动向