新版tomcat类加载机制改变导致的不同类加载器加载同一个类的实例赋值出错

出现问题

调用链部署过程中某个项目出现:

Caused by: java.lang.IllegalArgumentException: Can not set static XXX field redis.clients.jedis.Jedis.delegate$5975b70 to XXX
	at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)

Google 之后的结果不是很多,但是bytebuddy的一个Issue也出现了这个问题:

IllegalArgumentException: Can not set static XXX field YYY to XXX

当中解答有这么一句话:

The problam is that a class loaded by two different class loaders constitues two different types.

说白了就是同一个类被不同的类加载器加载了多遍,然后再赋值过程中就出现了上面的情况。 那么既然知道了出错的原因,接下来就要知道为什么会导致同一个类被加载多次。

现象梳理

现在的问题:

  • Can not set static com.lagou.pard.agent.plugin.intercept.ClassInstanceMethodInterceptor field redis.clients.jedis.Jedis.delegate$5975b70 to com.lagou.pard.agent.plugin.intercept.ClassInstanceMethodInterceptor

报错的原因:

  • ClassInstanceMethodInterceptor 类被不同类加载器加载然后进行赋值出错

是否代码问题:

  • 不是,尝试多个tomcat版本,7.0.40,7.0.6,7.0.8正常,7.0.56,7.0.78(线上版本)出现错误。
这里有个问题误导了我,我开始以为版本是这样的
7.0.40  OK
7.0.56  ERROR
7.0.6  OK
7.0.78  ERROR
7.0.8  OK
还奇怪为什么一会好一会坏,后来发现正确的应该是这样:
7.0.6  OK
7.0.8  OK
7.0.40  OK
7.0.56  ERROR
7.0.78  ERROR
7.0.8 是在7.0.78 之前的版本  7.0.8 != 7.0.80

调试:

理论上,根据 https://www.cnblogs.com/xing901022/p/4574961.html ,WEB-INF/lib下的jar文件中的class 由webapp 应用类加载器加载。

  • 7.0.6(正常tomcat)调试显示,两个ClassInstanceMethodInterceptor 都是由 Launcher$AppClassLoader 加载的-----这里和理论冲突(修正:了解之后发现tomcat启动之前的阶段类加载就是AppClassLoader)
  • 7.0.78(出错)调试显示,field.set(value)中,value的加载器是Launcher$AppClassLoader,但是field的加载器却是 WebappClassLoader

image.png

疑问:

  • tomcat的不同版本在类加载上有什么区别?并且是跨版本出现问题。
  • 正常的WEB-INF/lib下的jar文件中的class 由谁加载?应用类加载器
Tomcat:当应用需要到某个类时,则会按照下面的顺序进行类加载:
  1 使用bootstrap引导类加载器加载
  2 使用system系统类加载器加载
  3 使用应用类加载器在WEB-INF/classes中加载
  4 使用应用类加载器在WEB-INF/lib中加载
  5 使用common类加载器在CATALINA_HOME/lib中加载

Both AppClassLoader and SystemClassLoader are same.

application class loader : java.lang.ClassLoader.getSystemClassLoader() returns this loader

  • 理论上来说在WebAppClassLoader加载Interceptor的时候会先自己检查自己是否已经加载过(resourceEntries),然后去父级加载器寻找已经加载过的Class,如果还不存在才会由自己加载。

####尝试

  • 解构需要上线的项目:
    • 删除不需要的java、不需要的配置直到项目最简--保证错误依然出现
    • DEBUG WebAppClassLoader loadClass加载ClassInstanceMethodInterceptor的过程
clazz = this.findLoadedClass0(name);//自己查找一遍
...
clazz = this.findLoadedClass(name);//没找到用JVM再找一遍
...依然没找到
clazz = this.system.loadClass(name);//使用system加载
//这里的system是一个AppClassLoader
//加载成功,因为之前在javaagent阶段拦截tomcat类时已经加载过了
* 7.0.78 [7.0.78 loadClass代码图片](/getImage/IMG20180209172558DU7KPLGCGRJYUJXV3VI4FNNMSH91UA.png "7.0.78 loadClass代码")
clazz = this.findLoadedClass0(name);//自己查找一遍
...
clazz = this.findLoadedClass(name);//没找到用JVM再找一遍
...依然没找到
clazz = this.j2seClassLoader.loadClass(name);//使用j2seClassLoader加载
//这里的j2seClassLoader是一个Launcher$ExtClassLoader
//根据结构来说ExtClassLoader是AppClassLoader的父级,父级不能感知子级加载过的类,所以这里加载会失败,然后后面就开始用WebAppClassLoader自己加载了
* 其他
在7.0.78版本的WebAppClassLoader中发现了这个:
	/** @deprecated */
    @Deprecated
	protected ClassLoader system = null;
	所以明白为什么版本之间行为不一致(开始以为7.0.8和7.0.78是相隔很近的两个版本,其实差了好几年!!!!)。

####结论

  • 现在很清楚了,类加载机制不是一会好一会坏,而是新版本取消了system 这个ClassLoader;
  • 出现问题是因为WebAppClassLoader查找两次查找不到后使用j2seClassLoader加载时,j2seClassLoader(ExtClassLoader)不知道子级AppClassLoader已经加载过了。

####解决方案

  • 降级Tomcat:否定。tomcat新特性需要保留,不能因为agent降级tomcat,且线上原因不允许大动作。
  • 将agent.jar移出WEB-INF/lib 文件夹,这样就不会通过WebAppClassLoader加载
    • maven 自动化操作:尝试失败,各个项目war包打包有差别,不好统一操作。另外也没找到能将POM的某个jar移出lib目录的插件。
    • 手动:不再加入POM而是直接将jar包加入项目源码。更新需要手动更新jar文件。--这是一种不是很好的方。

....TODO

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×