0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

JVM源碼分析之不保證順序的Class.getMethods

冬至子 ? 來(lái)源:你假笨 ? 作者:寒泉子 ? 2023-06-27 15:22 ? 次閱讀

依賴順序的場(chǎng)景

如果大家看過(guò)或者實(shí)現(xiàn)過(guò)序列化反序列化的代碼,這個(gè)問(wèn)題就不難回答了,今天碰到的這個(gè)問(wèn)題其實(shí)是發(fā)生在大家可能最常用的fastjson庫(kù)里的,所以如果大家在使用這個(gè)庫(kù),請(qǐng)務(wù)必檢查下你的代碼,以免踩到此坑

對(duì)象序列化

大家都知道當(dāng)我們序列化好一個(gè)對(duì)象之后,要反序列回來(lái),那問(wèn)題就來(lái)了,就拿這個(gè)json序列化來(lái)說(shuō)吧,我們要將對(duì)象序列化成json串,那意味著我們要先取出這個(gè)對(duì)象的屬性,然后寫成鍵值對(duì)的形式,那取值就意味著我們要遵循java bean的規(guī)范通過(guò)getter方法來(lái)取,那其實(shí)getter方法有兩種,一種是boolean類型的,一種是其他類型的,如果是boolean類型的,那我們通常是isXXX()這樣的方法,如果是其他類型的,一般是getXXX()這樣的方法。那假如說(shuō)我們的類里針對(duì)某個(gè)屬性a,同時(shí)存在兩個(gè)方法isA()getA(),那究竟我們會(huì)調(diào)用哪個(gè)來(lái)取值?這個(gè)就取決于具體的序列化框架實(shí)現(xiàn)了,比如導(dǎo)致我們這篇文章誕生的fastjson,就是利用我們這篇文章的主角java.lang.Class.getMethods返回的數(shù)組,然后挨個(gè)遍歷,先找到哪個(gè)就是哪個(gè),如果我們的這個(gè)數(shù)組正好因?yàn)閖vm本身實(shí)現(xiàn)沒(méi)有保證順序,那么可能先找到isA(),也可能先找到getA(),如果兩個(gè)方法都是返回a這個(gè)屬性其實(shí)問(wèn)題也不大,假如正好是這兩個(gè)方法返回不同的內(nèi)容呢?

private A a;public A getA(){    return a;
}public boolean isA(){    return false;
}public void setA(A a){    this.a=a;
}

如果是上面的內(nèi)容,那可能就會(huì)悲劇了,如果選了isA(),那其實(shí)是返回一個(gè)boolean類型的,將這個(gè)boolean寫入到j(luò)son串里,如果是選了getA(),那就是將A這個(gè)類型的對(duì)象寫到j(luò)son串里

對(duì)象反序列化

在完成了序列化過(guò)程之后,需要將這個(gè)字符串進(jìn)行反序列化了,于是就會(huì)去找json串里對(duì)應(yīng)字段的setter方法,比如上面的setA(A a),假如我們之前選了isA()序列化好內(nèi)容,那我們此時(shí)的值是一個(gè)boolean值false,那就無(wú)法通過(guò)setA來(lái)賦值還原對(duì)象了。

解決方案

相信大家看完我上面的描述,知道這個(gè)問(wèn)題所在了,要避免類似的問(wèn)題,方案其實(shí)也挺多,比如對(duì)方法進(jìn)行先排序,又比如說(shuō)優(yōu)先使用isXXX()方法,不過(guò)這種需要和開(kāi)發(fā)者達(dá)成共識(shí),和setter要對(duì)應(yīng)得起來(lái)

jvm里為什么不保證順序

JDK層面的代碼我就暫時(shí)不說(shuō)了,大家都能看到代碼,從java.lang.Class.getMethods一層層走下去,相信大家細(xì)心點(diǎn)還是能抓住整個(gè)脈絡(luò)的,我這里主要想說(shuō)大家可能比較難看到的一些實(shí)現(xiàn),比如JVM里的具體實(shí)現(xiàn)

正常情況下大家跟代碼能跟到調(diào)用了java.lang.Class.getDeclaredMethods0這個(gè)native方法,其具體實(shí)現(xiàn)如下

JVM_ENTRY(jobjectArray, JVM_GetClassDeclaredMethods(JNIEnv *env, jclass ofClass, jboolean publicOnly))
{
  JVMWrapper("JVM_GetClassDeclaredMethods");  return get_class_declared_methods_helper(env, ofClass, publicOnly,                                           /*want_constructor*/ false,
                                           SystemDictionary::reflect_Method_klass(), THREAD);
}
JVM_END

其主要調(diào)用了get_class_declared_methods_helper方法

static jobjectArray get_class_declared_methods_helper(
                                  JNIEnv *env,
                                  jclass ofClass, jboolean publicOnly,
                                  bool want_constructor,
                                  Klass* klass, TRAPS) {

  JvmtiVMObjectAllocEventCollector oam;  // Exclude primitive types and array types
  if (java_lang_Class::is_primitive(JNIHandles::resolve_non_null(ofClass))
      || java_lang_Class::as_Klass(JNIHandles::resolve_non_null(ofClass))- >oop_is_array()) {    // Return empty array
    oop res = oopFactory::new_objArray(klass, 0, CHECK_NULL);    return (jobjectArray) JNIHandles::make_local(env, res);
  }  instanceKlassHandle k(THREAD, java_lang_Class::as_Klass(JNIHandles::resolve_non_null(ofClass)));  // Ensure class is linked
  k- >link_class(CHECK_NULL);

  Array< Method* >* methods = k- >methods();  int methods_length = methods- >length();  // Save original method_idnum in case of redefinition, which can change
  // the idnum of obsolete methods.  The new method will have the same idnum
  // but if we refresh the methods array, the counts will be wrong.
  ResourceMark rm(THREAD);
  GrowableArray< int >* idnums = new GrowableArray< int >(methods_length);  int num_methods = 0;  for (int i = 0; i < methods_length; i++) {    methodHandle method(THREAD, methods- >at(i));    if (select_method(method, want_constructor)) {      if (!publicOnly || method- >is_public()) {
        idnums- >push(method- >method_idnum());
        ++num_methods;
      }
    }
  }  // Allocate result
  objArrayOop r = oopFactory::new_objArray(klass, num_methods, CHECK_NULL);  objArrayHandle result (THREAD, r);  // Now just put the methods that we selected above, but go by their idnum
  // in case of redefinition.  The methods can be redefined at any safepoint,
  // so above when allocating the oop array and below when creating reflect
  // objects.
  for (int i = 0; i < num_methods; i++) {    methodHandle method(THREAD, k- >method_with_idnum(idnums- >at(i)));    if (method.is_null()) {      // Method may have been deleted and seems this API can handle null
      // Otherwise should probably put a method that throws NSME
      result- >obj_at_put(i, NULL);
    } else {
      oop m;      if (want_constructor) {
        m = Reflection::new_constructor(method, CHECK_NULL);
      } else {
        m = Reflection::new_method(method, UseNewReflection, false, CHECK_NULL);
      }
      result- >obj_at_put(i, m);
    }
  }  return (jobjectArray) JNIHandles::make_local(env, result());
}

從上面的k->method_with_idnum(idnums->at(i)),我們基本知道方法主要是從klass里來(lái)的

Method* InstanceKlass::method_with_idnum(int idnum) {
  Method* m = NULL;  if (idnum < methods()- >length()) {
    m = methods()- >at(idnum);
  }  if (m == NULL || m- >method_idnum() != idnum) {    for (int index = 0; index < methods()- >length(); ++index) {
      m = methods()- >at(index);      if (m- >method_idnum() == idnum) {        return m;
      }
    }    // None found, return null for the caller to handle.
    return NULL;
  }  return m;
}

因此InstanceKlass里的methods是關(guān)鍵,而這個(gè)methods的創(chuàng)建是在類解析的時(shí)候發(fā)生的

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    Handle protection_domain,
                                                    KlassHandle host_klass,
                                                    GrowableArray< Handle >* cp_patches,
                                                    TempNewSymbol& parsed_name,
                                                    bool verify,
                                                    TRAPS) {


...
 Array< Method* >* methods = parse_methods(access_flags.is_interface(),
                                            &promoted_flags,
                                            &has_final_method,
                                            &declares_default_methods,

...                                            CHECK_(nullHandle));// sort methodsintArray* method_ordering = sort_methods(methods); 
...
this_klass- >set_methods(_methods);
...
}

上面的parse_methods就是從class文件里挨個(gè)解析出method,并存到_methods字段里,但是接下來(lái)做了一次sort_methods的動(dòng)作,這個(gè)動(dòng)作會(huì)對(duì)解析出來(lái)的方法做排序

intArray* ClassFileParser::sort_methods(Array< Method* >* methods) {  int length = methods- >length();  // If JVMTI original method ordering or sharing is enabled we have to
  // remember the original class file ordering.
  // We temporarily use the vtable_index field in the Method* to store the
  // class file index, so we can read in after calling qsort.
  // Put the method ordering in the shared archive.
  if (JvmtiExport::can_maintain_original_method_order() || DumpSharedSpaces) {    for (int index = 0; index < length; index++) {
      Method* m = methods- >at(index);      assert(!m- >valid_vtable_index(), "vtable index should not be set");
      m- >set_vtable_index(index);
    }
  }  // Sort method array by ascending method name (for faster lookups & vtable construction)
  // Note that the ordering is not alphabetical, see Symbol::fast_compare
  Method::sort_methods(methods);

  intArray* method_ordering = NULL;  // If JVMTI original method ordering or sharing is enabled construct int
  // array remembering the original ordering
  if (JvmtiExport::can_maintain_original_method_order() || DumpSharedSpaces) {
    method_ordering = new intArray(length);    for (int index = 0; index < length; index++) {
      Method* m = methods- >at(index);      int old_index = m- >vtable_index();      assert(old_index >= 0 && old_index < length, "invalid method index");
      method_ordering- >at_put(index, old_index);
      m- >set_vtable_index(Method::invalid_vtable_index);
    }
  }  return method_ordering;
}// This is only done during class loading, so it is OK to assume method_idnum matches the methods() array// default_methods also uses this without the ordering for fast find_methodvoid Method::sort_methods(Array< Method* >* methods, bool idempotent, bool set_idnums) {  int length = methods- >length();  if (length > 1) {
    {
      No_Safepoint_Verifier nsv;
      QuickSort::sort< Method* >(methods- >data(), length, method_comparator, idempotent);
    }    // Reset method ordering
    if (set_idnums) {      for (int i = 0; i < length; i++) {
        Method* m = methods- >at(i);
        m- >set_method_idnum(i);
        m- >set_orig_method_idnum(i);
      }
    }
  }
}

從上面的Method::sort_methods可以看出其實(shí)具體的排序算法method_comparator

// Comparer for sorting an object array containing// Method*s.static int method_comparator(Method* a, Method* b) {  return a- >name()- >fast_compare(b- >name());
}

比較的是兩個(gè)方法的名字,但是這個(gè)名字不是一個(gè)字符串,而是一個(gè)Symbol對(duì)象,每個(gè)類或者方法名字都會(huì)對(duì)應(yīng)一個(gè)Symbol對(duì)象,在這個(gè)名字第一次使用的時(shí)候構(gòu)建,并且不是在java heap里分配的,比如jdk7里就是在c heap里通過(guò)malloc來(lái)分配的,jdk8里會(huì)在metaspace里分配

// Note: this comparison is used for vtable sorting only; it doesn't matter// what order it defines, as long as it is a total, time-invariant order// Since Symbol*s are in C_HEAP, their relative order in memory never changes,// so use address comparison for speedint Symbol::fast_compare(Symbol* other) const { return (((uintptr_t)this < (uintptr_t)other) ? -1
   : ((uintptr_t)this == (uintptr_t) other) ? 0 : 1);
}

從上面的fast_compare方法知道,其實(shí)對(duì)比的是地址的大小,因?yàn)镾ymbol對(duì)象是通過(guò)malloc來(lái)分配的,因此新分配的Symbol對(duì)象的地址就不一定比后分配的Symbol對(duì)象地址小,也不一定大,因?yàn)槠陂g存在內(nèi)存free的動(dòng)作,那地址是不會(huì)一直線性變化的,之所以不按照字母排序,主要還是為了速度考慮,根據(jù)地址排序是最快的。

綜上所述,一個(gè)類里的方法經(jīng)過(guò)排序之后,順序可能會(huì)不一樣,取決于方法名對(duì)應(yīng)的Symbol對(duì)象的地址的先后順序

JVM為什么要對(duì)方法排序

其實(shí)這個(gè)問(wèn)題很簡(jiǎn)單,就是為了快速找到方法呢,當(dāng)我們要找某個(gè)名字的方法的時(shí)候,根據(jù)對(duì)應(yīng)的Symbol對(duì)象,能根據(jù)對(duì)象的地址使用二分排序的算法快速定位到具體的方法。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    158

    瀏覽量

    12228
  • Symbol
    +關(guān)注

    關(guān)注

    1

    文章

    7

    瀏覽量

    9184
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    JVM性能指標(biāo)分析

    JVM性能調(diào)優(yōu)實(shí)踐——JVM
    發(fā)表于 10-17 15:00

    Jvm的整體結(jié)構(gòu)和特點(diǎn)

    、JVM特點(diǎn)    首先一次編譯處處運(yùn)行是學(xué)習(xí)Java語(yǔ)言都知道的事情,其實(shí)并不是Java語(yǔ)言跨平臺(tái),是JVM跨平臺(tái),Jvm運(yùn)行時(shí)并不是執(zhí)行Java文件,而是執(zhí)行編譯后的.class
    發(fā)表于 01-05 17:23

    Labview層疊式順序結(jié)構(gòu)

    Labview層疊式順序結(jié)構(gòu),很好的Labview資料,快來(lái)下載學(xué)習(xí)吧。
    發(fā)表于 04-19 10:56 ?0次下載

    Labview平鋪式順序結(jié)構(gòu)

    Labview平鋪式順序結(jié)構(gòu),很好的Labview資料,快來(lái)下載學(xué)習(xí)吧。
    發(fā)表于 04-19 10:56 ?0次下載

    Jvm工作原理學(xué)習(xí)筆記

    [] args)函數(shù)的class都可以作為JVM實(shí)例運(yùn)行的起點(diǎn) b) 運(yùn)行。main()作為該程序初始線程的起點(diǎn),任何其他線程均由該線程啟動(dòng)。JVM內(nèi)部有兩種線程:守護(hù)線程和非守護(hù)線程,main()屬于非守護(hù)
    發(fā)表于 04-03 11:03 ?5次下載

    JVM內(nèi)存布局的多方面了解

      JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證JVM的穩(wěn)定高效運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在部分差異。結(jié)合
    發(fā)表于 07-08 15:09 ?412次閱讀

    探討JVM的內(nèi)存布局

    JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證JVM的穩(wěn)定高效運(yùn)行。
    的頭像 發(fā)表于 09-09 15:57 ?829次閱讀

    JVM入門Class結(jié)構(gòu)介紹1

    ?根據(jù)《JAVA虛擬機(jī)規(guī)范》的規(guī)定,Class文件格式采用一種類似于C語(yǔ)言結(jié)構(gòu)體的偽結(jié)構(gòu)來(lái)存儲(chǔ)數(shù)據(jù),這種偽結(jié)構(gòu)中(Class文件格式中)只有兩種數(shù)據(jù)類型:“無(wú)符號(hào)數(shù)”和“表”。
    的頭像 發(fā)表于 02-09 16:57 ?619次閱讀
    <b class='flag-5'>JVM</b>入門<b class='flag-5'>之</b><b class='flag-5'>Class</b>結(jié)構(gòu)介紹1

    JVM入門Class結(jié)構(gòu)屬性表1

    ?屬性表在《JAVA虛擬機(jī)規(guī)范》中并沒(méi)有像其他數(shù)據(jù)一樣做嚴(yán)格的限制,我們甚至可以自己實(shí)現(xiàn)一個(gè)編譯器往Class結(jié)構(gòu)的屬性表中注入額外的屬性信息,虛擬機(jī)運(yùn)行時(shí)會(huì)忽略掉它識(shí)別不了的屬性。
    的頭像 發(fā)表于 02-10 11:02 ?576次閱讀
    <b class='flag-5'>JVM</b>入門<b class='flag-5'>之</b><b class='flag-5'>Class</b>結(jié)構(gòu)屬性表1

    JVM入門Class結(jié)構(gòu)屬性表2

    ?屬性表在《JAVA虛擬機(jī)規(guī)范》中并沒(méi)有像其他數(shù)據(jù)一樣做嚴(yán)格的限制,我們甚至可以自己實(shí)現(xiàn)一個(gè)編譯器往Class結(jié)構(gòu)的屬性表中注入額外的屬性信息,虛擬機(jī)運(yùn)行時(shí)會(huì)忽略掉它識(shí)別不了的屬性。
    的頭像 發(fā)表于 02-10 11:02 ?574次閱讀
    <b class='flag-5'>JVM</b>入門<b class='flag-5'>之</b><b class='flag-5'>Class</b>結(jié)構(gòu)屬性表2

    JVM內(nèi)存布局詳解

    JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證JVM的穩(wěn)定高效運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在部分差異。結(jié)合
    的頭像 發(fā)表于 04-26 10:10 ?528次閱讀
    <b class='flag-5'>JVM</b>內(nèi)存布局詳解

    詳解Java虛擬機(jī)的JVM內(nèi)存布局

    JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證JVM的穩(wěn)定高效運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在部分差異。結(jié)合
    的頭像 發(fā)表于 07-13 09:52 ?525次閱讀
    詳解Java虛擬機(jī)的<b class='flag-5'>JVM</b>內(nèi)存布局

    消息隊(duì)列中如何保證消息的順序性?

    其實(shí)這個(gè)也是用 MQ 的時(shí)候必問(wèn)的話題,第一看看你了不了解順序這個(gè)事兒?第二看看你有沒(méi)有辦法保證消息是有順序的?這是生產(chǎn)系統(tǒng)中常見(jiàn)的問(wèn)題。
    的頭像 發(fā)表于 09-08 09:40 ?728次閱讀
    消息隊(duì)列中如何<b class='flag-5'>保證</b>消息的<b class='flag-5'>順序</b>性?

    jvm的dump太大了怎么分析

    分析大型JVM dump文件可能會(huì)遇到的一些挑戰(zhàn)。首先,JVM dump文件通常非常大,可能幾百M(fèi)B或幾個(gè)GB。這是因?yàn)樗鼈儼?b class='flag-5'>JVM的完整內(nèi)存快照,包括堆和棧的所有對(duì)象和線程信息。
    的頭像 發(fā)表于 12-05 11:01 ?2674次閱讀

    jvm內(nèi)存分析命令和工具

    JVM內(nèi)存分析是Java開(kāi)發(fā)和調(diào)優(yōu)過(guò)程中非常重要的一部分。通過(guò)對(duì)JVM內(nèi)存分析命令和工具的深入了解和使用,可以幫助開(kāi)發(fā)人員識(shí)別內(nèi)存泄漏、性能瓶頸等問(wèn)題,并對(duì)Java應(yīng)用進(jìn)行優(yōu)化。 下面
    的頭像 發(fā)表于 12-05 11:07 ?1198次閱讀