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

概述

本文要說的內容是今天公司有個線上系統踩了一個坑,而且貌似還形成了必定的影響,後來系統相關的人定位到了是java.lang.Class.getMethods返回的順序可能不一樣機器不同,有問題的機器和沒問題的機器這個返回的方法列表是不同的,後面他們就來找到我求證是否jdk裏有這潛規則java

原本這個問題簡單一句話就能夠說明白,因此在晚上推送的消息裏也將這個事實告訴了你們,你們知道就好,之後不要再掉到坑裏去了,可是這個要細提及來其實也值得一說,因而在消息就附加了徵求你們意見的內容,看你們是否有興趣或者是否踩到過此坑,沒想到有這麼多人響應,表示對這個話題很感興趣,而且總結了你們問得最多的兩個問題是算法

  • 爲何有代碼須要依賴這個順序json

  • jvm裏爲何不保證順序數組

那這篇文章主要就針對這兩個問題展開說一下,另外之後針對此類可寫可不寫的文章先徵求下你們的意見再來寫可能效果會更好點,一來能夠回答你們的一些疑問(固然有些問題我也可能回答不上來,不過我儘可能去通讀代碼回答好你們),二來但願對我公衆號裏的文章繼續保持不求最多,只求最精的態度。app

爲了避免辜負你們的熱情,我連夜趕寫了這篇文章,若是你們以爲我寫的這些文章對你們有幫助,但願您能將文章分享出去,同時將個人公衆號你假笨推薦給您身邊更多的技術人,能幫助到更多的人去了解更多的細節,在下在此先謝過。框架

依賴順序的場景

若是你們看過或者實現過序列化反序列化的代碼,這個問題就不難回答了,今天碰到的這個問題實際上是發生在你們可能最經常使用的fastjson庫裏的,因此若是你們在使用這個庫,請務必檢查下你的代碼,以避免踩到此坑dom

對象序列化

你們都知道當咱們序列化好一個對象以後,要反序列回來,那問題就來了,就拿這個json序列化來講吧,咱們要將對象序列化成json串,那意味着咱們要先取出這個對象的屬性,而後寫成鍵值對的形式,那取值就意味着咱們要遵循java bean的規範經過getter方法來取,那其實getter方法有兩種,一種是boolean類型的,一種是其餘類型的,若是是boolean類型的,那咱們一般是isXXX()這樣的方法,若是是其餘類型的,通常是getXXX()這樣的方法。那假如說咱們的類裏針對某個屬性a,同時存在兩個方法isA()getA(),那究竟咱們會調用哪一個來取值?這個就取決於具體的序列化框架實現了,好比致使咱們這篇文章誕生的fastjson,就是利用咱們這篇文章的主角java.lang.Class.getMethods返回的數組,而後挨個遍歷,先找到哪一個就是哪一個,若是咱們的這個數組正好由於jvm自己實現沒有保證順序,那麼可能先找到isA(),也可能先找到getA(),若是兩個方法都是返回a這個屬性其實問題也不大,假如正好是這兩個方法返回不一樣的內容呢?jvm

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

若是是上面的內容,那可能就會悲劇了,若是選了isA(),那實際上是返回一個boolean類型的,將這個boolean寫入到json串裏,若是是選了getA(),那就是將A這個類型的對象寫到json串裏ide

對象反序列化

在完成了序列化過程以後,須要將這個字符串進行反序列化了,因而就會去找json串裏對應字段的setter方法,好比上面的setA(A a),假如咱們以前選了isA()序列化好內容,那咱們此時的值是一個boolean值false,那就沒法經過setA來賦值還原對象了。工具

解決方案

相信你們看完我上面的描述,知道這個問題所在了,要避免相似的問題,方案其實也挺多,好比對方法進行先排序,又好比說優先使用isXXX()方法,不過這種須要和開發者達成共識,和setter要對應得起來

jvm裏爲何不保證順序

JDK層面的代碼我就暫時不說了,你們都能看到代碼,從java.lang.Class.getMethods一層層走下去,相信你們細心點仍是能抓住整個脈絡的,我這裏主要想說你們可能比較難看到的一些實現,好比JVM裏的具體實現

正常狀況下你們跟代碼能跟到調用了java.lang.Class.getDeclaredMethods0這個native方法,其具體實現以下

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 其主要調用了`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裏來的

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是關鍵,而這個methods的建立是在類解析的時候發生的

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文件裏挨個解析出method,並存到_methods字段裏,可是接下來作了一次sort_methods的動做,這個動做會對解析出來的方法作排序

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能夠看出其實具體的排序算法是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()); } 

比較的是兩個方法的名字,可是這個名字不是一個字符串,而是一個Symbol對象,每一個類或者方法名字都會對應一個Symbol對象,在這個名字第一次使用的時候構建,而且不是在java heap裏分配的,好比jdk7裏就是在c heap裏經過malloc來分配的,jdk8裏會在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方法知道,其實對比的是地址的大小,由於Symbol對象是經過malloc來分配的,所以新分配的Symbol對象的地址就不必定比後分配的Symbol對象地址小,也不必定大,由於期間存在內存free的動做,那地址是不會一直線性變化的,之因此不按照字母排序,主要仍是爲了速度考慮,根據地址排序是最快的。

綜上所述,一個類裏的方法通過排序以後,順序可能會不同,取決於方法名對應的Symbol對象的地址的前後順序

JVM爲何要對方法排序

其實這個問題很簡單,就是爲了快速找到方法呢,當咱們要找某個名字的方法的時候,根據對應的Symbol對象,能根據對象的地址使用二分排序的算法快速定位到具體的方法。

 

推薦閱讀:

Java 虛擬機進程狀態管理工具 jps 失效?嚇尿了!

JVM Code Cache空間不足,致使服務性能變慢

相關文章
相關標籤/搜索