JVM源码分析之不保证顺序的Class.getMethods

概述

本文要说的内容是今天公司有个线上系统踩了一个坑,并且貌似还造成了一定的影响,后来系统相关的人定位到了是java.lang.Class.getMethods返回的顺序可能不同机器不一样,有问题的机器和没问题的机器这个返回的方法列表是不一样的,后面他们就来找到我求证是否jdk里有这潜规则

本来这个问题简单一句话就可以说明白,所以在晚上推送的消息里也将这个事实告诉了大家,大家知道就好,以后不要再掉到坑里去了,但是这个要细说起来其实也值得一说,于是在消息就附加了征求大家意见的内容,看大家是否有兴趣或者是否踩到过此坑,没想到有这么多人响应,表示对这个话题很感兴趣,并且总结了大家问得最多的两个问题是

  • 为什么有代码需要依赖这个顺序
  • jvm里为什么不保证顺序

那这篇文章主要就针对这两个问题展开说一下,另外以后针对此类可写可不写的文章先征求下大家的意见再来写可能效果会更好点,一来可以回答大家的一些疑问(当然有些问题我也可能回答不上来,不过我尽量去通读代码回答好大家),二来希望对我公众号里的文章继续保持不求最多,只求最精的态度。

为了不辜负大家的热情,我连夜赶写了这篇文章,如果大家觉得我写的这些文章对大家有帮助,希望您能将文章分享出去,同时将我的公众号你假笨推荐给您身边更多的技术人,能帮助到更多的人去了解更多的细节,在下在此先谢过。

依赖顺序的场景

如果大家看过或者实现过序列化反序列化的代码,这个问题就不难回答了,今天碰到的这个问题其实是发生在大家可能最常用的fastjson库里的,所以如果大家在使用这个库,请务必检查下你的代码,以免踩到此坑

对象序列化

大家都知道当我们序列化好一个对象之后,要反序列回来,那问题就来了,就拿这个json序列化来说吧,我们要将对象序列化成json串,那意味着我们要先取出这个对象的属性,然后写成键值对的形式,那取值就意味着我们要遵循java bean的规范通过getter方法来取,那其实getter方法有两种,一种是boolean类型的,一种是其他类型的,如果是boolean类型的,那我们通常是isXXX()这样的方法,如果是其他类型的,一般是getXXX()这样的方法。那假如说我们的类里针对某个属性a,同时存在两个方法isA()getA(),那究竟我们会调用哪个来取值?这个就取决于具体的序列化框架实现了,比如导致我们这篇文章诞生的fastjson,就是利用我们这篇文章的主角java.lang.Class.getMethods返回的数组,然后挨个遍历,先找到哪个就是哪个,如果我们的这个数组正好因为jvm本身实现没有保证顺序,那么可能先找到isA(),也可能先找到getA(),如果两个方法都是返回a这个属性其实问题也不大,假如正好是这两个方法返回不同的内容呢?

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串里

对象反序列化

在完成了序列化过程之后,需要将这个字符串进行反序列化了,于是就会去找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空间不足,导致服务性能变慢

原文地址:https://www.cnblogs.com/perfma/p/12405008.html

时间: 2024-08-26 09:31:22

JVM源码分析之不保证顺序的Class.getMethods的相关文章

JVM源码分析之堆外内存完全解读

概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存

JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)

概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalReference,本文主要想讲的是FinalReference,因为我们在使用内存分析工具比如mat等在分析一些oom的heap的时候,经常能看到 java.lang.ref.Finalizer占用的内存大小远远排在前面(其实通过jmap -histo就能发现,如下图所示),而这个类占用的内存大小又

JVM源码分析之SystemGC完全解读

概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可以通过jmap来触发等,针对每个场景其实我们都可以写篇文章来做一个介绍,本文重点介绍下System.gc的原理 或许大家已经知道如下相关的知识 system.gc其实是做一次full gc system.gc会暂停整个进程 system.gc一般情况下我们要禁掉,使用-XX:+DisableExplicitGC system.gc在cms

JVM源码分析之栈溢出完全解读

概述 之所以想写这篇文章,其实是因为最近有不少系统出现了栈溢出导致进程crash的问题,并且很隐蔽,根本原因还得借助coredump才能分析出来,于是想从JVM实现的角度来全面分析下栈溢出的这类问题,或许你碰到过如下的场景: 日志里出现了StackOverflowError的异常 进程突然消失了,但是留下了crash日志 进程消失了,crash日志也没有留下 这些都可能是栈溢出导致的. 如何定位是否是栈溢出 上面提到的后面两种情况有可能不是我们今天要聊的栈溢出的问题导致的crash,也许是别的一

JVM源码分析之javaagent原理完全解读

概述 本文重点讲述javaagent的具体实现,因为它面向的是我们Java程序员,而且agent都是用Java编写的,不需要太多的C/C++编程基础,不过这篇文章里也会讲到JVMTIAgent(C实现的),因为javaagent的运行还是依赖于一个特殊的JVMTIAgent. 对于javaagent,或许大家都听过,甚至使用过,常见的用法大致如下: java -javaagent:myagent.jar=mode=test Test 我们通过-javaagent来指定我们编写的agent的jar

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.ClassLoader源码分析 11.自定义类加载器 12.加载类的三种方式 13.总结 14.特别注意 @ 前言 你是否真的理解java的类加载机制?点进文章的盆友不如先来做一道非常常见的面试题,如果你能做出来,可能你早已掌握并理解了java的类加载机制,若结果出乎你的意料,那就很有必要来了解了解j

源码分析:onAttach, onMeasure, onLayout, onDraw 的顺序。

从前文<源码解析:dialog, popupwindow, 和activity 的第一个view是怎么来的?>中知道了activity第一个view或者说根view或者说mDecorView 其实就是一个FrameLayout,以及是在系统handleResume的时候加入到系统windowManager中的,并由framework中的ViewRootImpl 接管,通过ViewRootImpl.setView() 开始整个显示过程的.这次着重梳理一下view的显示过程(onAttach, o

AtomicInteger源码分析——基于CAS的乐观锁实现

AtomicInteger源码分析--基于CAS的乐观锁实现 1. 悲观锁与乐观锁 我们都知道,cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换.切换涉及到清空寄存器,缓存数据.然后重新加载新的thread所需数据.当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notify(),notifyAll()唤醒回来.在某个资源不可用的时候,就将cpu让出,把当前等待线程切换为阻

Java线程池ThreadPoolExector的源码分析

前言:线程是我们在学习java过程中非常重要的也是绕不开的一个知识点,它的重要程度可以说是java的核心之一,线程具有不可轻视的作用,对于我们提高程序的运行效率.压榨CPU处理能力.多条线路同时运行等都是强有力的杀手锏工具.线程是如此的重要,那么我们来思考这样一个问题.假设我们有一个高并发,多线程的项目,多条线程在运行的时候,来一个任务我们new一个线程,任务结束了,再把它销毁结束,这样看似没有问题,适合于低并发的场景,可是当我们的项目投入到生产环境,一下涌入千条任务的时候,线程不断的new执行