天天看點

你知道Java方法能定義多少個參數嗎?一:為什麼研究這麼無聊的問題二:實地考察三:一切都結束了

一:為什麼研究這麼無聊的問題

這兩天在讀一本老書《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 + ",");
        }
    }           

完美解放了生産力 。

生成完參數清單,定義好方法,當我滿懷信心的開始編譯時,編譯器給了我狠狠一刀:

你知道Java方法能定義多少個參數嗎?一:為什麼研究這麼無聊的問題二:實地考察三:一切都結束了

居然不是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呢?我覺得主要有以下兩種辦法:

  1. 修改Javac源碼,幹掉以上參數限制這一段代碼,再重新編譯;
  2. 利用位元組碼修改工具,硬改位元組碼,加上一個擁有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個參數的方法而保證不被人打死嗎。

有人可能要問,如果我定義的方法參數是變長參數呢?還有這種限制嗎?這當然是沒有的,因為變成參數的本質其實就是傳遞一個數組,你傳再多的參數,編譯後其實都隻是一個數組而已。

其實Java不是限制方法的參數個數,而是限制參數的機關數量,我又回頭看了下源碼,Javac在編譯期計算方法的參數個數時,調用的是這個方法:

Code.width(types.erasure(env.enclMethod.sym.type).getParameterTypes())

Code.width()會根據參數類型,計算出以位元組為機關的參數數量:

public static int width(int typecode) {
        switch (typecode) {
        case LONGcode: case DOUBLEcode: return 2;
        case VOIDcode: return 0;
        default: return 1;
        }
    }           

可見對于long、doubule,會占用兩個機關數,是以說java方法最多隻能有255個參數是不準确的,隻能說java方法容許的參數總機關數為255。其實想想也對,java方法調用前會将參數進行壓棧,虛拟機本身就應該基于方法傳參占用的棧深度去進行限制。

另有知友提醒,《Java虛拟機規範》中其實已經做了說明,我翻了一下,在《Java虛拟機限制》這一小節中有提到:

方法的參數最多有255個,它是由方法描述符(§4.3.3)的定義所限制,如果方法調 用是針對執行個體或接口方法,那麼這個限制也包􏰁着占有一個單元的 this。注意對于定義 在方法描述符中的參數長度來說,每個 long 和 double 參數都會占用兩個長度機關,所 以如果有這些類型的話,最終的限制的最大值将會變小。

書中自有黃金屋啊!不過紙上得來終覺淺,絕知此事要躬行,你說是不是呢?

三:一切都結束了

嗯,做完實驗,寫完文章,我總算把這件事搞明白了,女朋友早已在呼呼大睡,好像我确實很無聊,好像我确實還是一條鹹魚。

聽說喜歡點關注的同學都長得帥