简单记录一次远古版本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对象
https://img2022.cnblogs.com/blog/1368608/202207/1368608-20220716174136288-690276328.png
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,看有没有相类似的问题, 还真的被我搜出来了,
https://img2022.cnblogs.com/blog/1368608/202207/1368608-20220716201002154-420017167.png
继续点进去发现了一些很有意思的东西
(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项目比较老, 每次都要去加载类的信息到永久代中, 时间久了, 永久代就挂了
https://img2022.cnblogs.com/blog/1368608/202207/1368608-20220716201901270-1462170331.png
既然找到了问题所在, 改的话, 也就简单了, 直接打开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
而且这次真的是体会到了一活跃的开源社区的强大之处, 要是一个使用的人都比较少的框架, 都没几个人, 靠自己很难发现问题的所在之处, 并不是每个人都有修改源码的能力的, 害
再一次提醒我们要多提高技术, 有时间就关注开源社区的一些问题以及最新动向
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页:
[1]