我是靠谱客的博主 有魅力故事,最近开发中收集的这篇文章主要介绍记一次PermGen持续增长的解决过程前言背景Falcon监控原因初步排查JVM优化原因进一步排查总结参考,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

前言


这个问题的起因,是一次PermGen持续增长的报警,而问题的解决,是PermGen、类加载、Spring、JDK等知识的融合。

本次问题,从发生到最终解决,经历了很长的时间,这个过程中,有工程方法,有JVM优化,有源码追踪,有思想碰撞,最终解决问题后,发现是Spring 4.1.1.RELEASE的BUG,从4.2.4之后,这个BUG就修复了,这次问题的解决,对于我们技术提高,在某个时刻突破某种瓶颈,深有裨益。

问题的解决,是多位同事合作的结果,我再一次总结一遍,以求加深理解,并对读者有所帮助。


背景


SRE给jvm.memory.perm.used.percent增加报警,于是,开始不断收到报警。


报警之后,我们用jstat -gccause -h10 pid 1000,监控进程,发现PermGen满了之后,又被FullGC回收掉了,下一步,我们需要查出PermGen持续增长的原因。


Falcon监控


永久代





GC





Load Class




原因初步排查


TraceClassLoading


到底是什么原因导致了PermGen持续增长呢,猜不到,就采用一些帮助定位问题的方法。

在应用启动参数中增加 -XX:+TraceClassLoading ,每加载一个类,都会加载一个类,于是,我们看到:

[plain]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. [Loaded sun.reflect.GeneratedSerializationConstructorAccessor10037 from __JVM_DefineClass__]  
  2. [Loaded sun.reflect.GeneratedSerializationConstructorAccessor10038 from __JVM_DefineClass__]  


MAT


这还有一种看类的办法,首先通过 jmap -dump:format=b,file=xxx pid dump堆,然后通过MAT工具查看,如下图。



求助Google,http://rednaxelafx.iteye.com/blog/548536,发现如下代码和日志吻合。




BTrace


到底是什么调用了这个代码呢,通过Btrace打出调用栈,定位出产生问题的业务代码。Btrace代码如下。

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. @BTrace  
  2. public class ClassloadTester {  
  3.        
  4.     @OnMethod(clazz="java.lang.reflect.Constructor", method="acquireConstructorAccessor")  
  5.     public static void defineclass(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {  
  6.         print(Strings.strcat("entered ", probeClass));  
  7.         println(Strings.strcat(".", probeMethod));  
  8.         jstack();  
  9.         println("==========================");  
  10.     }  
  11. }  

原因初步找到了,就是一个ApplicationContext.getBean操作,从Spring容器中拿出一个Prototype类,由于这个类被设计成了有状态的,所以不能用Singleton。

原因似乎找到了,但还是有诸多疑问,原型类多个实例没问题,难道会有多个Class被生成,如果这样,那岂不是所有使用了Spring容器存储Prototype类的场景,都会有问题,并且,我们也没有看到不断Load我们业务相关的代理类啊?

这些疑问当时都被搁置了,想对业务类进行非状态改造,发现工作量挺大,于是暂时搁置了。


JVM优化


老报警也不是个事,直接关掉报警又太low,这时查到两个JVM参数:

-XX:+CMSPermGenSweepingEnabled

-XX:CMSInitiatingPermOccupancyFraction=70

CMS接管Perm,70%时并发进行垃圾收集,避免满了再FullGC。

通过图示,我们可以看到,未加这两个参数之前,在 PermGen 满了之后,执行了full gc,回收了新生代、老年代、永久代。


增加了permgen参数,在70%的时候,进行垃圾回收,回收了新生代、永久代,不会回收老年代,如下图。



PermGen 90%报警,我70%就给你回收了,领导也不会收到报警了,这其实没有根本解决问题,只是歪门邪道:)只是比直接关掉报警强一点。


原因进一步排查


对编程相关的知识有怀疑,千万不要空想,也不要长时间停留在问Google的阶段,写一段代码验证一下,是一个好办法。

于是,有同事开始写一段和业务代码非常类似的代码,但是简单很多,方便改动、调试、执行。

最最重要的,通过简单的代码,我们可以定位到是在哪行代码之后出现问题,接着,我们就能知道为什么在这行代码之后会出现问题。


看代码调用链

  1. ApplicationContext.getBean("errorService", ErrorService.class)
  2. AbstractApplicationContext.getBean
  3. AbstractRefreshableApplicationContext.getBeanFactory
  4. AbstractBeanFactory.getBean、doGetBean
  5. AbstractAutowireCapableBeanFactory.createBean、doCreateBean、createBeanInstance、instantiateBean、applyBeanPostProcessorsAfterInitialization
  6. CglibAopProxy.getProxy
  7. ObjenesisCglibAopProxy.createProxyClassAndInstance
  8. ObjenesisBaseget.newInstance、getInstantiatorOf
  9. StdInstantiatorStrategy.newInstantiatorOf
  10. NativeMethodAccessorImpl.invoke
  11. DelegatingMethodAccessorImpl.invoke
  12. Method.invoke
  13. NativeMethodAccessorImpl.invoke
  14. MethodAccessorGenerator.generate
  15. ClassDefiner.defineClass

然后,“Loaded sun.reflect.GeneratedSerializationConstructorAccessor10037 from __JVM_DefineClass__” 出现了。


说几个重点代码


1、在Prototype标注的类上使用aop注入后,spring在生成aop代理时,是每次都会生成一个新的class吗?



我们可以看到,Enhancer每次都调用createClass()方法,但是,生成类时使用了cache,保证同一个类加载器里面,一个类同样的增强配置时,只有生成一个类。


2、产生问题的代码截图










3.产生问题的根本原因

通过上面的截图,我们看到,这个类"sun.reflect.GeneratedSerializationConstructorAccessor10037",是Objenesis帮我们生成对象的过程中加载的。

查询Objenesis的相关资料,原来它是Spring用来实例化类的,用它实例化类有很多好处,可以看官方文档。

难道用Objenesis实例化类,每次都要加载这个类吗?继续看官方说明,可以看到:


原来官方推荐单例使用的,而Spring4.1.1中用错了,非单例的;CglibAopProxy是通过ProxyFactory来生成的,因此每次都会new一个,所以Objenesis也会new一个。试着用Spring的更高版本,发现从4.2.4开始,Spring已经修正了这个Bug,改成静态的了。



强调一下


这个问题是ObjenesisCglibAopProxy中的Bug引起的,而在Spring4.2.4开始,已经修复了这个Bug。

什么情况下,我们会踩到这个Bug?

当服务声明为Prototype,并且方法上有注解存在时,因为只有这种场景,才有使用到ObjenesisCglibAopProxy。


代码演示


https://github.com/pumadong/cl-roadshow/tree/master/roadshow-app/src/main/java/com/cl/roadshow/spring/perm


另一个15次代码优化问题


我们在Debug过程中,发现另外一种现象,截图如下:


即,每当一个类从Sping容器中取出15次后,第16次取出的时候,也会加载这样一个类,为什么呢?

看这个帖子:http://rednaxelafx.iteye.com/blog/548536 。


总结


这个问题,主要经过两个阶段:

第一个阶段,使用工程方法,比如Open-Falcon监控、Jstack、JMap、MAT、TraceClassLoading、BTrace,定位出问题所在;

第二个阶段,在找不出根本原因的情况下,化繁为简,通过简单的演示代码,复现问题,并通过Debug的细致跟踪,找出Spring 4.1.1在“ObjenesisCglibAopProxy”中,使用“ObjenesisStd”实例化类时的Bug。

第一个阶段重点是工程方法,分析问题的步骤;第二个阶段重点是代码分析,通过Debug逐步定位到问题代码。

处理过程中的应该做出的推断:

1、“Loaded sun.reflect.GeneratedSerializationConstructorAccessor10037 from __JVM_DefineClass__”,这个提示是很明显的,虽然PermGen满了,但是是被这个类填充的,不是我们的业务类,所以,我们不应该怀疑是我们的业务类被增强而产生了多个;因为没有我们的业务类被增强并被Loaded的提示;所以,只要找出这个类加载的原因就好了;

2、Btrace已经帮我们定位了调用栈,所以如果想从根本上解决问题,尽早写测试代码来重现问题即可,错误不重现,不能说找到了根本原因,那么如果在生产改了,很可能产生错误的解决办法。


参考


http://hg.openjdk.java.net/jdk7u/jdk7u/jdk/file/70e3553d9d6e/src/share/classes/sun/reflect/MethodAccessorGenerator.java

http://rednaxelafx.iteye.com/blog/548536

http://objenesis.org/tutorial.html

http://objenesis.org/

JVM中Load过多Class的分析


来自:http://blog.csdn.net/puma_dong/article/details/51895055

最后

以上就是有魅力故事为你收集整理的记一次PermGen持续增长的解决过程前言背景Falcon监控原因初步排查JVM优化原因进一步排查总结参考的全部内容,希望文章能够帮你解决记一次PermGen持续增长的解决过程前言背景Falcon监控原因初步排查JVM优化原因进一步排查总结参考所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(46)

评论列表共有 0 条评论

立即
投稿
返回
顶部