Android开发中关于虚拟机那些事,让你豁然开朗

0761fa00aa90c5c96ddaa49683a0a562.jpeg

/   今日科技快讯   /

近日腾讯在线上召开内部员工大会,同时有大约100多名员工现场参与听会,今年大会的主题是降本增效。对于PCG部分业务的改革,“你活都活不下去了,周末还休闲的打球”,马化腾隔空喊话称,“那留给它们的时间不多了”,如果业务团队不能给自己压力,那么由总办来给这个压力。

/   作者简介   /

明天周六啦,天气越来越冷,大家注意做好防护!

本篇文章来自小红的投稿,文章主要分享了虚拟机的相关知识,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

小红的博客地址:

https://www.zhihu.com/people/xiao-hong-35-2-62/posts

/   前言   /

作为豁然开朗篇的最终篇,本文要讲解的是虚拟机这块,因为在之前讲解内存与线程的时候,一直都会牵涉到虚拟机和指令集这块,所以,为了让大家再豁然开朗多一次,本文会从解虚拟机和dex指令以及klass模型等这些地方来带大家深入了解安卓的虚拟机的。

/   虚拟机   /

虚拟机(JVM)的作用是将Java源码编译成字节码文件.class文件,并且运行这些字节码文件,为什么可以运行呢,因为它可以将这些字节码解读(翻译)成不同平台(系统)对应的机器码,从而能运行。而Dalvik和Art是安卓中使用的虚拟机。

1. Jvm虚拟机与Android虚拟机的区别

一个Java文件通过JVM虚拟机调用javac编译成.class文件,然后虚拟机运行该字节码文件时,就是将里面的字节码翻译成机器指令供硬件去运行。而在安卓体系里,一个Java文件通过虚拟机调用javac编译成.class文件后,会用dex工具去将该.class文件编译成.dex文件,然后这些.dex文件又会被打包成apk文件,当安装apk文件时,安卓虚拟机就会运行里面的.dex文件,将.dex里面的字节码翻译成机器指令供硬件去运行:

362f06e6d61334b0b50d432fb52707eb.png

机器指令就是机器码,一种CPU的可读指令,所以它是CPU用来调度各硬件的指令;字节码比机器码要抽象多一层,由一序列操作码代码/数据组成的二进制文件,在jvm解释器使用JIT编译器编译成本机机器代码后才供硬件运行。

而汇编代码是一种宏语言,相当于机器码的一种人类可读形式。它经过转译最终也会成为机器码,所以它和字节码是同一级别的,汇编语言可以简单理解为机器语言的助记符,便于人们理解和记忆。

可能会有人疑问为什么jvm不直接对java文件编译成机器指令?因为jvm是跨平台的。无论在哪种操作系统上执行,都可以转成对应的机器语言(这是字节码转机器指令的过程),不仅如此,因为jvm只认识class字节码文件,所以其他语言只需要编译成class文件就可以使用jvm来编译执行了。这仅仅是从跨平台的优点来讲,把所写的源代码编译成class文件后按照规划区分不同的部分,这样也有利于虚拟机高效方便在内存里管理对象与运行程序。

2. Art虚拟机与Dalvik虚拟机的区别

安卓5.0之前的虚拟机是Dalvik虚拟机,使用的是JIT编译,即每次运行程序,都实时地进行将部分dex字节码编译成机器指令供手机去运行,这样整个apk包占用系统内存会很小,但因为每次运行都需要编译,所以CPU的消耗则变大。

5.0之后安卓虚拟机就变成ART虚拟机,使用AOT编译,即在应用安装期间,就将全部dex文件编译成机器指令存储在手机上。这样手机运行app时就可以直接运行这些机器指令,不需要像以前Dalvik虚拟机那样还要再去编译,这样就可以使得整个app运行过程速度要快很多。当然与此同时app可能会比以前要占用的内存要大,但现在的安卓手机的内存已经发展的越来越大了,所以这个缺点是可以忽略不计的。

3. class文件与dex文件的区别

我们都知道一个class文件就是一个类文件,有多少个类则生成多少个文件,而一个dex文件是包含了很多文件,如图所示:

6b17ea6c31a4a70d7f56b21caf369ac4.jpeg

这个dex文件是解压apk文件后得到的,查看classes.dex里面是包含很多文件的,所以,对比class文件来说,使用dex方式可以减少文件数,从而减少很多冗余数据,减少占用的内存大小。

class文件和dex文件的结构也是不一样:

27e5d3f46baaf6678957b7dea5ef7b45.png

可以看到,dex文件比class文件里的数据不一样,class文件里更多表示的是一个类里的每个结构部分,而dex文件的结构不一样,里面细划分为不同部分结构的集合。从一个jar包和一个apk包对比来看,jar包里会有很多class文件,因为一个类对应一个class文件,而一个apk文件就一个或者几个dex文件就可以表示所有我们写的java文件了:

82c8d62bdc5be09f79bfe41db80f4fdc.png

class文件有header、常量池以及其他结构的数据,而每个class文件里的这些结构数据,到了被编译dex文件时,都被分类成每个不同类型的文件集合里,然后这些文件集合就相当于dex文件里的各个结构属性文件(header、string_ids、method_ids等),所以这也是为什么说一个dex文件就包含了多个class的原因了。

所以其实dex这种压缩性好(指令密集能节省内存空间)的特性是适合于移动端这种内存小的系统,而像PC这种内存大的则更适合使用class文件(指令短小所以能快速执行)。

4.栈跟寄存器的概念

栈是在内存里划分的一块连续空间,从上到下依次排下去,然后数据先进后出,每次当一个数据要入栈时,栈就会向上腾出一个空间,让该数据入栈,下一个数据入栈时,也是如此操作,而当栈底的数据出栈时,实则栈的一个类似于指针的标志位变量会往下调,让它存储为栈底的那个数据的值,这样就相当于把原先那个栈顶的数据弹出栈了,而只剩底部的数据,这样一直上调下调(load和store)来控制变量入栈和出栈:

180acd2044d9e0ab68a21889b930234d.png

81b07794c12f07abd89781630fbca279.png

而寄存器是相当于CPU里的内存,而它是有固定的内存地址的,所以数据在它那里存取是非常快的,距离CPU又近,所以直接根据它的内存地址便可取到数据。

寄存器的指令长度较长,但一个指令能存储的数据也多,但也容易丢失,但总体来看,可以用数量更少的指令去完成操作。而栈的指令则跟寄存器的情况相反,指令短小,携带数据也变少,但能快速执行

最后还要补充的一点就是Android虚拟机是基于寄存器的虚拟机,而jvm虚拟机是基于虚拟栈的虚拟机。(寄存器和虚拟机栈的更多详情可以去观阅我的 豁然开朗篇:安卓开发中关于内存那些事 以及豁然开朗篇:安卓开发中关于线程那些事(上篇)(https://zhuanlan.zhihu.com/p/578946126) )。所以dex文件里的指令跟class文件里的指令相比,dex指令要长而且数量要少,这也是dex文件与class文件的区别。

5. 程序执行的原理

当一个java文件被编译成.class文件后,直接用记事本打开它,是一堆乱码,因为此时它里面都是一些字节码,所以要使用javap工具去打开它,javap会去对它进行翻译,即编译成机器指令。比如这段代码:

public class Person {

    public static void run() {
        int a = 1;
        int b = 2;
        int c = a*b;
    }
}

我们把这个java文件编译成class文件后,再用javap工具对它编译成机器指令:

cd424fa43422f45d91287012ff410785.jpeg

可以看到输入命令后输出了一堆指令集,这些指令便是class文件里面的乱码经过编译后变成的机器指令了,其中源码中我们写的Person类的run()方法对应的指令便是这段:

public static void run();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=0
         0: iconst_1
         1: istore_0
         2: iconst_2
         3: istore_1
         4: iload_0
         5: iload_1
         6: imul
         7: istore_2
         8: return
 ....

0-8行的指令集可以描述整个run方法是怎样执行的:

1)iconst_1表示把常量1加载到操作数栈

2)istore_0表示把操作数栈的1存储到局部变量表的第0个位置上

3)iconst_2表示把常量2加载到操作数栈里

4)istore_1表示把操作数栈的2存储到局部变量表的第1个位置上

5)iload_0表示加载局部变量第0个变量的值压入到操作数栈

6)iload_1表示加载局部变量第1个变量的值压入到操作数栈

7)imul表示操作数栈中的前两个变量相乘,并将结果压入操作数栈顶

8)istore_2表示把操作数栈的2(imul指令的结果)存储到局部变量表的第2个位置上

9)return表示返回空(因为return后面没有数据,表示返回空)

纵观下来,其实这些指令并不难懂,如果对这块感兴趣的可自行去搜索学习。以上这个过程其实我在 豁然开朗篇:安卓开发中关于内存那些事 (https://zhuanlan.zhihu.com/p/578946126)已经讲过,不过当时并未讲它们的指令,只从内存的结构去分析一个方法的运行,因此现在再重新以指令的角度去阐释一个方法的运行,再结合之前的内存结构知识去理解,相信大家对于虚拟机的认识又上升了一个台阶了。

而同样的代码在安卓系统下,则是会被编译成arm机器指令。当java文件编译成.class文件后,然后安卓虚拟机使用dx.bat工具把.class文件编译成.dex文件:

8876ea007c44029d2557ebe6f95b19b9.png

输入上面的命令,然后成功将Person类的run()方法编程后的dex指令输出到Person.dex.txt文件里,我们来查看它:

Person.run:()V:
regs: 0003; ins: 0000; outs: 0000
  ...
  0000: const/4 v0, #int 1 // #1
  ...
  0001: const/4 v1, #int 2 // #2
  ...
  0002: mul-int v2, v0, v1
 ...
  0004: return-void
  ...

已经把一些不需要关心的指令给忽略掉,可以明显看到,这四行指令就是整个run方法执行的过程:

1)const/4 v0, #int 1表示把int类型的常量1(const/4:位数为4)存入到v0寄存器中

2)const/4 v1, #int 2表示把int类型的常量2(const/4:位数为4)存入到v1寄存器中

3)mul-int v2, v0, v1表示把v0寄存器上的值跟v1寄存器的值相乘的结果存入到v2寄存器中

4)return-void表示返回空值

其实arm机器指令也是很好理解,对应的指令是什么意思可以网上去搜索,这里就不细讲了。

通过以上的class文件编译后的机器指令跟dex文件编译后的arm指令对比之后,可以发现,同样的代码(java代码),JVM生成的机器指令要9行,而安卓虚拟机生成的则4行,不过你也可以看到,JVM的每条指令简短,而安卓虚拟机的每条指令则较长。而且安卓虚拟机的每条指令占用的大小比JVM的每条指令要大。从语义上来讲,arm指令要比jvm的机器指令要好理解

/   Klass   /

在 豁然开朗篇:安卓开发中关于线程那些事(上篇)中讲锁的对象的内存结构时提到过klass,在程序里对Person进行断点:

532adb6d22ce20496fa3944f5d8162fd.jpeg

通过断点信息可以看到person对象的klass_:

cd3b4ce7932695f30624b2c24eaad4e2.jpeg

klass其实是person对象在内存里的映射,它里面包含着一个对象里各种信息的集合:

e48465b870a52b5be1ccfa2a4c2e1427.jpeg

所以对于虚拟机的视角来说,它看到的对象其实是klass,klass才是真正的一个类模板,在内存中存储着该类的各种信息以及它们自己所处的地址。

1.起源

在一个方法里构造对象:

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);


        Person person = new Person();
        person.run();
    }
}

从上面分析指令的时候可以知道,类里所写的方法其实就是一堆指令集,既然如此,也是要有内存空间来存储这些指令的,而这些指令集就是存在方法区里。首先方法区里的onCreate方法指令集执行,然后开辟虚拟机栈,在栈帧里入栈onCreate()方法,此时局部变量表里就存储person变量,然后随着 Person person = new Person()的执行,堆区里构造出new Person()对象,最后让person引用指向new Person()对象:

730d019253df92fbcc451e0e7e57abf0.png

Person person = new Person()这句代码我们分成两部分来看,一部分是Person person,另一部分则是new Person()。Person person是个引用,可以理解为指针,用来存储该对象的内存地址的。当new Person()执行完后,就在堆里开辟了该对象,然后把实例数据填充进去,填充的自然是变量和方法表地址等,而这些数据是从方法区里的类信息和方法表里加载进去的,最后把那个指针指向该对象,所以这时通过“person.run()、person.age”方式访问对象和调用对象方法时,通过person引用找到Person()对象,然后在Person()对象里找到age变量,又因为对象头里有方法表地址,所以顺着该地址找到方法区里的方发表,然后根据对象名以及相关信息可以找到ArtMethod表里关于run方法的地址(指针),虚拟机就开始执行该方法的指令集:

eaded18a73779090e00171e670a88eaf.png

整个过程清晰明了,关键的地方在于类信息的属性以及方法表里关于这个类的方法的地址等这些信息要加载给我们的对象里,这样我们调用对象的属性和方法才能调用到,那这个加载是怎样进行的,其实就是把klass里关于这个类的信息的属性以及这个类的方法的地址等这些信息加载给java层的Object的过程,下面来分析这个通信过程。

2.FindClass过程

使用过反射来创建class对象都知道这个方法:

Class pClass1 = Class.forName(className);

跟踪它里面的源码可以知道它是native方法:

/** Called after security checks have been made. */
    @FastNative
    static native Class<?> classForName(String className, boolean shouldInitialize,
            ClassLoader classLoader) throws ClassNotFoundException;

如果使用过jni开发过,应该也知道它里面也有一个可以构建class对象的函数:

extern "C" JNIEXPORT jstring JNICALL
Java_com_pingred_myapplication3_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    ...
    env->FindClass("com/pingred/myapplication3/Person")
    ...
}

无论是new Person(),还是使用Class.forName("Person"),亦或是在jni里调用env->FindClass("Person"),这三种方式去构建Person对象,最终都是会调用到在目录\android-8.0.0_r1\art\runtime下的类链接器http://class_linker.cc的FindClass()方法来构建对象的:

mirror::Class* ClassLinker::FindClass(Thread* self,
                                      const char* descriptor,
                                      Handle<mirror::ClassLoader> class_loader) {
  DCHECK_NE(*descriptor, '\0') << "descriptor is empty string";
  DCHECK(self != nullptr);
  self->AssertNoPendingException();
  self->PoisonObjectPointers();  // For DefineClass, CreateArrayClass, etc...
  if (descriptor[1] == '\0') {
    // only the descriptors of primitive types should be 1 character long, also avoid class lookup
    // for primitive classes that aren't backed by dex files.
    return FindPrimitiveClass(descriptor[0]);
  }
  const size_t hash = ComputeModifiedUtf8Hash(descriptor);
  // Find the class in the loaded classes table.
  ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, hash, class_loader.Get());
  if (klass != nullptr) {
    return EnsureResolved(self, descriptor, klass);
  }
  ...

代码很长,先来一点点来分析,首先就留意到ObjPtr类型的klass(这就证明了对于虚拟机底层来说一个类对象是klass,而java层来说则是class对象),然后调用了LookupClass()函数构建出klass,根据注释知道从一个类表里去查找这个class对象,然后赋值给klass变量,那很明显这个类表是一个缓存的作用,然后如果类表里查不到,则往下的逻辑应该是去构造新的klass了,接着往下看是否是如此:

mirror::Class* ClassLinker::FindClass(Thread* self,
                                      const char* descriptor,
                                      Handle<mirror::ClassLoader> class_loader) {
 ...
  const size_t hash = ComputeModifiedUtf8Hash(descriptor);
  // Find the class in the loaded classes table.
  ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, hash, class_loader.Get());
  if (klass != nullptr) {
    return EnsureResolved(self, descriptor, klass);
  }
  // Class is not yet loaded.
  if (descriptor[0] != '[' && class_loader == nullptr) {
    // Non-array class and the boot class loader, search the boot class path.
    ClassPathEntry pair = FindInClassPath(descriptor, hash, boot_class_path_);
    if (pair.second != nullptr) {
      return DefineClass(self,
                         descriptor,
                         hash,
                         ScopedNullHandle<mirror::ClassLoader>(),
                         *pair.first,
                         *pair.second);
    }
    ...
  }
  ...

可以看到后面果然是调用了DefineClass()函数构建新的klass,来看看这个函数的详情:

mirror::Class* ClassLinker::DefineClass(Thread* self,
                                        const char* descriptor,
                                        size_t hash,
                                        Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,
                                        const DexFile::ClassDef& dex_class_def) {
  StackHandleScope<3> hs(self);
  auto klass = hs.NewHandle<mirror::Class>(nullptr);

  // Load the class from the dex file.
  if (UNLIKELY(!init_done_)) {
    // finish up init of hand crafted class_roots_
    if (strcmp(descriptor, "Ljava/lang/Object;") == 0) {
      klass.Assign(GetClassRoot(kJavaLangObject));
    } else if (strcmp(descriptor, "Ljava/lang/Class;") == 0) {
      klass.Assign(GetClassRoot(kJavaLangClass));
    } else if (strcmp(descriptor, "Ljava/lang/String;") == 0) {
      klass.Assign(GetClassRoot(kJavaLangString));
    } else if (strcmp(descriptor, "Ljava/lang/ref/Reference;") == 0) {
      klass.Assign(GetClassRoot(kJavaLangRefReference));
    } else if (strcmp(descriptor, "Ljava/lang/DexCache;") == 0) {
      klass.Assign(GetClassRoot(kJavaLangDexCache));
    } else if (strcmp(descriptor, "Ldalvik/system/ClassExt;") == 0) {
      klass.Assign(GetClassRoot(kDalvikSystemClassExt));
    }
  }
  ...

同样代码很长,先来看开始的部分,可以看到,构建新的klass对象前先清空,把klass的内存空间置为0,再接着看下面的代码:

mirror::Class* ClassLinker::DefineClass(Thread* self,
                                        const char* descriptor,
                                        size_t hash,
                                        Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,
                                        const DexFile::ClassDef& dex_class_def) {

  ...

// Load the fields and other things after we are inserted in the table. This is so that we don't
  // end up allocating unfree-able linear alloc resources and then lose the race condition. The
  // other reason is that the field roots are only visited from the class table. So we need to be
  // inserted before we allocate / fill in these fields.
  LoadClass(self, *new_dex_file, *new_class_def, klass);
  if (self->IsExceptionPending()) {
    VLOG(class_linker) << self->GetException()->Dump();
    // An exception occured during load, set status to erroneous while holding klass' lock in case
    // notification is necessary.
    if (!klass->IsErroneous()) {
      mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
    }
    return nullptr;
  }
  ...

可以看到调用了LoadClass函数,这个函数传的参数有new_dex_file、new_class_def以及klass,很明显这个函数其实就是把dex文件里的各种关于这个类的数据都加载到klass对象里:

void ClassLinker::LoadClass(Thread* self,
                            const DexFile& dex_file,
                            const DexFile::ClassDef& dex_class_def,
                            Handle<mirror::Class> klass) {
  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
  if (class_data == nullptr) {
    return;  // no fields or methods - for example a marker interface
  }
  LoadClassMembers(self, dex_file, class_data, klass);
}

里面先调用了dex_file.GetClassData()函数把dex文件里关于该类的信息数据放进class_data里,然后再次调用LoadClassMembers()函数,把dex_file, class_data, klass都传进去:

void ClassLinker::LoadClassMembers(Thread* self,
                                   const DexFile& dex_file,
                                   const uint8_t* class_data,
                                   Handle<mirror::Class> klass) {
  {
    ...

    // Load instance fields.
    LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,
                                                                allocator,
                                                                it.NumInstanceFields());
    ...

    // Set the field arrays.
    klass->SetSFieldsPtr(sfields);
    DCHECK_EQ(klass->NumStaticFields(), num_sfields);
    klass->SetIFieldsPtr(ifields);
    DCHECK_EQ(klass->NumInstanceFields(), num_ifields);
    // Load methods.
    bool has_oat_class = false;
    const OatFile::OatClass oat_class =
        (Runtime::Current()->IsStarted() && !Runtime::Current()->IsAotCompiler())
            ? OatFile::FindOatClass(dex_file, klass->GetDexClassDefIndex(), &has_oat_class)
            : OatFile::OatClass::Invalid();
    const OatFile::OatClass* oat_class_ptr = has_oat_class ? &oat_class : nullptr;
    klass->SetMethodsPtr(
        AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),
        it.NumDirectMethods(),
        it.NumVirtualMethods());
   ...
    }
    for (size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
      ArtMethod* method = klass->GetVirtualMethodUnchecked(i, image_pointer_size_);
      LoadMethod(dex_file, it, klass, method);
      DCHECK_EQ(class_def_method_index, it.NumDirectMethods() + i);
      LinkCode(this, method, oat_class_ptr, class_def_method_index);
      class_def_method_index++;
    }
    ...

可以看到该函数里都是各种for循环遍历dex文件里的集合,然后调用klass->SetSFieldsPtr()等这些klass的函数,把从dex文件里存储的关于该类的变量集合、方法表地址等数据加载进去klass里,其中可以看到ifields变量表这样的集合,对应上文中dex文件结构图的field_ids。然后method方法则通过klass->GetVirtualMethodUnchecked()构建之后,再调用LoadMethod()把方法指令集和它对应的klass类、访问权限和以及执行地址等信息设置到ArtMethod里:

void ClassLinker::LoadMethod(const DexFile& dex_file,
                             const ClassDataItemIterator& it,
                             Handle<mirror::Class> klass,
                             ArtMethod* dst) {
  uint32_t dex_method_idx = it.GetMemberIndex();
  const DexFile::MethodId& method_id = dex_file.GetMethodId(dex_method_idx);
  const char* method_name = dex_file.StringDataByIdx(method_id.name_idx_);

  ScopedAssertNoThreadSuspension ants("LoadMethod");
  dst->SetDexMethodIndex(dex_method_idx);
  dst->SetDeclaringClass(klass.Get());
  dst->SetCodeItemOffset(it.GetMethodCodeItemOffset());

  dst->SetDexCacheResolvedMethods(klass->GetDexCache()->GetResolvedMethods(), image_pointer_size_);

  uint32_t access_flags = it.GetMethodAccessFlags();
  ...

}

所以平时使用反射invoke获取Method方法底层实质上就是会从ArtMethod表里根据所传的字符串来遍历然后找到方法,这样会比较费时,而通过对象.方法名的方式调用方法,因为事先对象已经跟klass关联了,也就不用遍历ArtMethod表就能直接通过方法指令集的执行地址像指针那样直接定位到这个方法,所以就比反射的方式高效。

来看看art_method.h文件就可以知道ArtMethod是长怎样的:

class ArtMethod FINAL {
 public:
  // May cause thread suspension due to GetClassFromTypeIdx calling ResolveType this caused a large
  // number of bugs at call sites.
  mirror::Class* GetReturnType(bool resolve) REQUIRES_SHARED(Locks::mutator_lock_);

  mirror::ClassLoader* GetClassLoader() REQUIRES_SHARED(Locks::mutator_lock_);
  ...
  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;

  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;

  /* End of dex file fields. */

  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // ifTable.
  uint16_t method_index_;

  // The hotness we measure for this method. Managed by the interpreter. Not atomic, as we allow
  // missing increments: if the method is hot, we will see it eventually.
  uint16_t hotness_count_;

  // Fake padding field gets inserted here.

  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
    ArtMethod** dex_cache_resolved_methods_;

    // Pointer to JNI function registered to this method, or a function to resolve the JNI function,
    // or the profiling data for non-native methods, or an ImtConflictTable, or the
    // single-implementation of an abstract/interface method.
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;
  ...

后面还有属性和方法就省略掉了,可以看到方法的类、访问权限和执行地址等信息,entry_point_from_quick_compiled_code_指针就是存储执行指令的地址,有了它就可以在内存中定位到方法指令集。现在我们就可以知道在java源文件里我们声明类的方法,在虚拟机底层里的表现就是ArtMethod类,所以也可以说在java里定义的方法它的大小并不是由多少写了多少行代码来决定的,而是由这个ArtMethod类的大小决定的。

/   总结   /

豁然开朗篇暂时就告一段落,写该系列的初衷除了让大家明白自己所写的代码在内存里是怎样呈现之外,还想激发大家对安卓系统的兴趣,因为你也看到我们一深入虚拟机底层的时候,它里面都是c/c++文件,以及暗含各种内存、操作系统等这些基础知识,所以该系列作为研究安卓系统源码前的热身学习就再好不过了。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

由浅入深,详解 LiveData 的那些事

App处于前台,Activity就不会被回收了?

欢迎关注我的公众号

学习技术或投稿

a9ffc8e1fa9bb85550d6b24ce5df59c0.png

2595135807adaebcff1070a498fbda2a.jpeg

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值