最近同事开发一个新交易的时候,使用了谷歌表达式引擎aviator,在测试阶段碰到了一个诡异的问题:应用在本机环境上测试,功能一切正常,丢到websphare服务器上测试,只要调用了相关交易,应用进程会立刻挂掉然后被websphare的nodeagent重新拉起来。由于这个交易是一个文件入库功能,最开始我怀疑是OOM导致的,毕竟文件解析时没控制好,很容易导致死循环嘛。在分析了javacore.txt、core.dmp、headdump.phd后,发现堆内存一切正常,GC一切正常,甚至连线程都一切正常,没有死锁等异常情况。
于是我怀疑是不是搞错了方向,便重新开始看起了javacore文件,之前一直急于查找与OOM有关的蛛丝马迹,加之是第一次接触websphare,可能漏了一些其他线索。最后在“Current Thread”这里发现了原因,调用栈如下:
Native Stack中调用了动态库libj9prt28,j9这个词很熟悉,查了一下原来是IBM的J9虚拟机,与我们一般使用的hotspot不同。所以我怀疑是这个动态库的问题,在google上直接拿“libj9prt28.so+0x”搜索,发现IBM官网上一些与这个库相关的BUG反馈帖子,但是情况又不尽相同。最后很无奈,只能回归aviator源码,看看在ClassDefiner.defineClass中到底做了什么,毕竟更往下就调用到了rt.jar中的Unsafe了。源码如下:
在defineClass这个方法中,有两种加载类的方式,其中“DEFINE_CLASS_HANDLE.invokeExact”是通过“MethodHandle”机制调用到与其绑定的“Unsafe.defineAnonymousClass”方法,这与线程堆栈保持了一致:
而进入到该分支的判断条件“if (!preferClassLoader && DEFINE_CLASS_HANDLE != null)”,可以推断出为true,因为:
- preferClassLoader值通过System.getProperty("aviator.preferClassloaderDefiner", "false")获取,我们目前没有设置该系统参数,所以肯定为false,则“!preferClassLoader”为true。
- DEFINE_CLASS_HANDLE成员变量在类ClassDefiner启动时就初始化了,堆栈显示调用到Unsafe中,因此“DEFINE_CLASS_HANDLE != null”也为true。
最后回到堆栈信息,调用到Native Stack后,java进程,即虚拟机进程直接挂掉,因此defineClass中关于异常捕获的代码逻辑也没起到作用。为了测试另外一条分支是否可行,我在websphare的启动参数添加“-Daviator.preferClassloaderDefiner=true”,重启java应用后,进行测试,成功。
备注:在网上查了一下Unsafe.defineAnonymousClass这个方法,与ClassLoader一样用于加载类,引用博客《Java魔法类:Unsafe应用解析》(链接https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html)中的解释:“可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过Unsafe.defineAnonymousClass方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何ClassLoader下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的Class对象时,该类就会被GC回收。故而VM Anonymous Class相比于Java语言层面的匿名内部类无需通过ClassClassLoader进行类加载且更易回收”。ClassLoader加载的类也有类卸载机制,只不过条件很苛刻,在我的应用中,需要对编译后的aviator表达式做JVM级缓存,且表达式不会太多,因此不用考虑类数量爆炸导致metaspace溢出的问题。至于为什么Unsafe.defineAnonymousClass会出错,由于本人对J9虚拟机没有更深入的了解,因此不得而知了。