昨天潇桐提到他在一台日常机器上遇到了个奇怪的错误:
1 |
Invocation of init method failed; nested exception is java.lang.VerifyError: ( class : com/taobao/tddl/client/jdbc/TDataSourceConfig$ 3 , method: $VRi signature: ()[[Z) Stack size too large |
他看到这个错误之后搜了一下,猜测是不是要通过-Xss参数将方法调用栈调大一些来解决。
但答案是:不,跟-Xss完全没有关系。引发这个错误的直接原因只有一种:Class文件的内容不正确。
首先请留意抛出的异常是java.lang.VerifyError。这个异常只会在类加载过程中的“校验”步骤抛出,意思是该Class文件的内容未能通过校验器的校验。换句话说,抛出这个异常的时候,它所说的类尚未完成加载,其中的方法自然也不可能被执行,所以这里跟调整方法调用栈的大小不会有关系。
那么这里的“stack size”到底是什么呢?想看例子的话,请参考之前我发在自己的blog上的演示稿,JVM分享20100621.pdf,在第63页开始的几页中,我用几张示意图讲解了JVM中方法调用栈与操作数栈的关系。
详细内容这里就略过不讲了,需要提的是:-Xss控制的是每个线程的方法调用栈的大小,而上面的错误信息里的“stack size”说的是某个方法所需要的操作数栈的大小。Java源码被编译为Class文件后,每个Java方法所需要使用的“局部变量区”大小与“操作数栈”大小都是由编译器计算好的一个固定值,并且会记录在Class文件中。大家可以通过javap -verbose命令来查看某个方法需要使用的局部变量区与操作数栈大小。例如:
javap -verbose java.lang.Object
例如可以看到Object上的equals方法的字节码上有Stack=2, Locals=2, Args_size=2的字样,说明该方法需要使用2个单位的空间用作操作数栈,2个单位的空间用作局部变量区(其中2个单位用于保存参数)。每调用一次Object.equals()方法,在开始调用时就会在线程的方法调用栈上分配一个“栈帧”用于存储方法的活动记录,这个栈帧里包括了该方法需要的局部变量区、操作数栈以及其它一些VM需要的信息(例如返回地址等)。操作数栈只是方法调用栈里的一块块小部分而已。
那么操作数栈大小跟VerifyError有什么关系呢?在JVM加载类的过程中,需要对加载进来的Class文件做校验。其中,校验器会扫描每个方法里的每条字节码指令,看看指令内容与方法声称需要使用的局部变量区/操作数栈大小是否匹配(实际使用量不得超出声称的最大量)。JVM指令集的设计本身已经考虑到要便于校验,所以校验器可以在某个方法被实际执行之前通过查看每条指令来计算出这个方法在实际运行中会需要使用的局部变量区/操作数栈大小。
例如,“iconst_0”指令用于将一个常量0压到操作数栈上。该指令对栈的影响是:
... -> ..., 0
这种记法的阅读方法:箭头左右两侧分别表示指令执行前后操作数栈的状况;每一侧以右边为操作数栈顶,...表示不关心值的状况(并且执行指令前后这些值没有改变)。上面所写的就是,执行iconst_0之前操作数栈上可能有一些值了(但我们不管那些是什么),执行指令后常量0被压到栈顶,而栈顶下面还是之前原有的值没变。这就说明执行iconst_0指令需要让操作数栈的使用量增加1单位。
再看一个例子,“iadd”指令会把栈顶的两个值弹出来,做加法,然后把结果压回到栈顶。它对栈的影响是:
..., value1, value2 -> ..., result
也就是说该指令消耗2单位然后使用1单位的操作数栈空间,整体来看就是消耗了1单位。
组合起来看,i = 0 + 1;用字节码可以表示为(这里仅为示意;Java语言的话这样的代码会先变成i = 1,加法就没了):
iconst_0; iconst_1, iadd; istore_0
其中,操作数栈的“高度”变化为:
1 |
指令 : 栈高度 : 栈内容(右边为栈顶) |
2 |
0 : [ ] |
3 |
iconst_0 : 1 : [ 0 ] |
4 |
iconst_1 : 2 : [ 0 , 1 ] |
5 |
iadd : 1 : [ 1 ] |
6 |
istore_0 : 0 : [ ] |
这样的一段字节码需要使用的操作数栈的最大高度就是2。记录在Class文件里的信息指的就是这个。
顺便来张图:
(注意到虽然执行过程中操作数栈与局部变量区实际用了的部分在变,但分配空间是以需要的固定的最大大小来一口气分配的。
例如说把局部变量写在循环里面还是外面其实都没什么关系,对性能有关系的只是“运算”有多少。)
知道是“哪个栈”了,那为什么会有VerifyError呢?潇桐说他在自己的开发机上跑那个应用没有遇到错误,于是我用javap -verbose命令查看了一下他使用的tddl-client-2.3.3.1.jar的内容,看到没什么问题,也根本没有调用过错误信息中所说的$VRi方法。然后把出问题的机器上的同一个JAR包下载下来,同样用javap来看,发现JAR包内容与二方库中的不一样,看起来是被post-weaving过,而且不幸的把Class文件的内容改坏了。
具体是什么工具使得JAR包内容发生了变化我还不清楚,后续会再更新到这里。不过一般会做post-weaving的会是一些AOP工具或者混淆工具;这些工具用的时候都要很小心,不然会很容易把Class文件弄坏,请大家也多加留心。