一: 为什么研究这么无聊的问题
这两天在读一本老书Orange'S 一个操作系统的实现, 把丢了很长时间没研究的操作系统又重新十起来了, 在第三章讲解" 保护模式 " 时, 作者提到了调用门描述符中的 Param Count 只有 5 位, 也就是说, 最多只支持 32 个参数, 这本来只是一个不是特别重要的细节, 但是却勾起了我的思索: 在 JVM 中, 一个 Java 方法, 最多能定义多少参数呢? 我知道这是一个很无聊的问题, 即使能定义一万个, 十万个, 谁又会真的去这么做呢. 但是作为一个 Coder, 最重要的不就是好奇心吗, 没有好奇心, 和一条咸鱼又有什么区别呢?
二: 实地考察
这种问题, 第一步当然就是看看 JVM 中关于方法的定义, 这里以 openJDK10 中的 HotSpot 为例. 在 ConstMethod 中, 代表参数数量的字段为_size_of_parameters.
u2 _size_of_parameters; // size of the parameter block (receiver + arguments) in words
_size_of_parameters 的类型为 u2, 在 JVM 中, u2 为 2 个字节长, 那么理论上来说, HotSpot 支持的方法最大参数数量为 2^16 - 1, 即 65535.
这个答案究竟是否正确呢? 实践出真知!
当然我不会傻到真的去一个个定义 65535 个参数, 那我岂不成了 "数一亿粒米" 的幼儿园老师了? Coder 就得按照 Coder 的办法:
- public static void main(String[] args) {
- for (int i = 0; i <65535; i++) {
- System.out.print("int a" + i + ",");
- }
- }
完美解放了生产力 .
生成完参数列表, 定义好方法, 当我满怀信心的开始编译时, 编译器给了我狠狠一刀:
居然不是 65535? 那应该是多少呢? 难道是一个字节长? 废话不多说, 我立即来实验了下 255 个参数, 编译通过, 再试了一下 256, 和 65535 时一样报错. 那么结果很明显了, Java 方法最多可以定义 255 个参数.
我查看了下 Javac 源码, 在生成方法的字节码时, 有方法参数数量限制判断:
- if (Code.width(types.erasure(env.enclMethod.sym.type).getParameterTypes()) + extras> ClassFile.MAX_PARAMETERS) {
- log.error(tree.pos(), "limit.parameters");
- nerrs++;
- }
其中 ClassFile.MAX_PARAMETERS = 255.
事情到这里我很不甘心, HotSpot 中明明是用两个字节长来定义的方法参数数量, 莫非只是 Javac 在编译过程中做了限制? 只要能成功编译出一个有 256 个参数的 java 方法, 在虚拟机中一试便知, 但是怎么才能绕过 Javac 呢?
我觉得主要有以下两种办法:
一: 修改 Javac 源码, 干掉以上参数限制这一段代码, 再重新编译;
二: 利用字节码修改工具, 硬改字节码, 加上一个拥有 256 个参数的方法.
第一种方法看似简单, 但是其实从 openJDK 中提取出来的 Javac 项目不能直接 run, 需要很多配置, 而且源码依赖了很多 jdk 中的不可见类, 操作起来很麻烦. 所以这里我采用了第二种方法, 工具选用的是老朋友 javassist.
其实 javassist 使用起来很简单, 这里我只需要对一个已有的 class 文件加上一个新方法即可:
- try {
- StringBuilder sb = new StringBuilder();
- sb.append("public static void testMax(");
- for (int i = 0; i <256; i++) {
- sb.append("int a" + i);
- if(i < 255) {
- sb.append(",");
- }
- }
- sb.append("){}");
- ClassPool cPool = new ClassPool(true);
- cPool.insertClassPath("/Users/wanginbeijing/Documents/MyProgramings/java/Mine/test/src");
- CtClass cClass = cPool.get("com.wangxiandeng.test.Test");
- CtMethod newMethod = CtNewMethod.make(sb.toString(), cClass);
- cClass.addMethod(newMethod);
- cClass.writeFile("/Users/wanginbeijing/Documents/MyProgramings/java/Mine/test/src");
- } catch (NotFoundException e) {
- e.printStackTrace();
- } catch (CannotCompileException e) {
- e.printStackTrace();
- } catch (
- IOException e) {
- e.printStackTrace();
- }
以上就通过 javassist 成功的给 Test.class 文件加上了一个拥有 256 个参数的方法 testMax(). 现在让我们运行下 Test.class 试试:
java com.wangxiandeng.test.Test
没想到这次虽然瞒过了编译器, 却没有过的了虚拟机这一关, 运行直接报错了:
错误: 加载主类 com.wangxiandeng.test.Test 时出现 LinkageError
java.lang.ClassFormatError: Too many arguments in method signature in class file com/wangxiandeng/test/Test
看样子 Java 不仅仅在编译期会对方法参数数量做限制, 在虚拟机运行期间同样会干这件事. 本着一查到底的精神, 我在 HotSpot 源码中搜索了下上面报的错误, 找到了虚拟机检查参数数量的地方:
- Method* ClassFileParser::parse_method(const ClassFileStream* const cfs,
- bool is_interface,
- const ConstantPool* cp,
- AccessFlags* const promoted_flags,
- TRAPS) {
- ......
- if (_need_verify) {
- args_size = ((flags & JVM_ACC_STATIC) ? 0 : 1) +verify_legal_method_signature(name, signature, CHECK_NULL);
- if (args_size> MAX_ARGS_SIZE) {
- classfile_parse_error("Too many arguments in method signature in class file %s", CHECK_NULL);
- }
- }
- ......
- }
可见虚拟机在解析 class 文件中的方法时, 会判断参数数量 args_size 是否大于 MAX_ARGS_SIZE, 如果大于则就会报错了. MAX_ARGS_SIZE 为 255.
这里有一点需要注意, 在计算 args_size 时, 有判断方法是否为 static 方法, 如果不是 static 方法, 则会在方法原有参数数量上再加一, 这是因为非 static 方法会添加一个默认参数到参数列表首位: 方法的真正执行者, 即方法所属类的实例对象.
事情到这里总算大概明白了, Java static 方法的参数最多只能有 255 个, 非 static 方法最多只能有 254 个. 虽然远不及我刚开始推测的 65535 个, 但是这也完全够用了, 毕竟你敢在你的项目里定义一个 255 个参数的方法而保证不被人打死吗.
有人可能要问, 如果我定义的方法参数是变长参数呢? 还有这种限制吗? 这当然是没有的, 因为变成参数的本质其实就是传递一个数组, 你传再多的参数, 编译后其实都只是一个数组而已.
一切都结束了
嗯, 做完实验, 写完文章, 我总算把这件事搞明白了, 女朋友早已在呼呼大睡, 好像我确实很无聊, 好像我确实还是一条咸鱼.
来源: https://juejin.im/entry/5b947add6fb9a05cf67a6af8