精华内容
下载资源
问答
  • 深入解析ANDROID虚拟机

    2016-04-28 08:14:30
    本书十三个章节,分别讲解了android系统基础知识,android系统的结构和核心框架,Java虚拟机和Dalvik虚拟机的知识,实现程序编译和调试,Dalvik的运作流程,DEX优化和安全管理,Android虚拟机生命周期的管理和内存...
  • android虚拟机非常深奥,这本书是市面上少有的深入解析android虚拟机的书籍,是android进阶不二的选择。
  • 钟世礼-深入解析ANDROID虚拟机(带书签完整高清版) 好资料
  • 本文主要讲解Android虚拟机动态调试背后涉及到的技术原理,除了JDWP协议细节,还包括任意位置断点、堆栈输出、变量值获取等基础调试功能的具体实现。另外本文提供了一款新的android动态调试工具——AVMDBG,提供调试...
  • 本书循序渐进地讲解了Android虚拟机系统的基本知识,并剖析了其整个内存系统的进程和运作流程,并对虚拟机系统优化和异常处理的知识进行了详细讲解。本书几乎涵盖了Dalvik VM系统的所有主要内容,并且讲解方法通俗...
  • 主要介绍了unity3d发布apk在android虚拟机中运行的详细步骤,需要的朋友可以参考下
  • 本书循序渐进地讲解了Android虚拟机系统的基本知识,并剖析了其整个内存系统的进程和运作流程,并对虚拟机系统优化和异常处理的知识进行了详细讲解。本书几乎涵盖了Dalvik VM系统的所有主要内容,并且讲解方法通俗...
  • 掌握Java和Android虚拟机

    千次阅读 2020-08-15 23:24:19
    ​我们知道的虚拟机有很多,运行Java的JVM虚拟机,运行Android程序的Davlik和Art虚拟机,运行C#的CLR虚拟机,那么什么是虚拟机呢,虚拟机的作用又是什么呢?运行JavaScript的v8引擎或者运行Python的引擎是否也是...

    ​我们知道的虚拟机有很多,运行Java的JVM虚拟机,运行Android程序的Davlik和Art虚拟机,运行C#的CLR虚拟机,那么什么是虚拟机呢,虚拟机的作用又是什么呢?运行JavaScript的v8引擎或者运行Python的引擎是否也是虚拟机呢?带着这几个问题,我们开始对虚拟机的学习。

    虽然现在很多人都认为运行JavaScript的V8或运行Python的VirtualEnv,都不是虚拟机,而是解释器,主要原因是因为V8或者VirtualEnv不仅仅能执行字节码文件,还能将源文件编译成字节码文件,而传统上定义的虚拟机只是用来运行字节码文件的,如果将源文件编译成字节码,则需要编译器来帮忙,比如在JVM虚拟机上运行的文件都是已经编译成字节码的class文件,但是V8或者Python,都能一边编译源代码,一边执行编译后的字节码文件。但是现在这个规范已经越来越宽松了,也有不少大神认为V8或者VirtualEnv也是虚拟机。

    那么一个虚拟机具备什么样的能力呢?我们下面就来具体看看吧。

    1. 将源码编译成字节码(编译器能力)
    2. 装载字节码文件(加载,链接,初始化)
    3. 内存管理
      • 运行时内存区域
      • 垃圾回收
    4. 指令解析和执行

    接下来主要以JVM,Davlik和Art三款虚拟机为例,分别介绍上述的能力。

    将源码编译成字节码

    class字节码

    java的字节码文件是通过java编译器来生成的,我们下载jdk后,通过javac命令,就可以将一个java源文件生成java字节码文件,这个字节码文件就可以直接在JVM上面运行了。

    编译器通过对源代码进行词法,语法,语义分析,生成抽象语法树(AST),然后根据AST,生成目标文件。

    在这里插入图片描述

    词法,语法,语义这一流程不是java编译器独有的,是所有的编译器都共有的能力,不管是llvm编译c文件,或者是我们解析如html,xml等dsl文件,都是这样的步骤。解析完成后的字节码文件如下。
    在这里插入图片描述

    我简单介绍一下class字节码文件的内容结构

    • Header:文件头包含了magic(魔数)——“验证是否是class格式文件”;minor_version,major_version——“该class文件支持的版本等数据信息”
    • Constant Pool:常量池包含了类中所有的资源信息,如字面量常量——”字符串,被final修饰的常量等“;符号引用——”类和接口的全限定(绝对路径)名;字段的名称和描述符;方法的名称和描述符“
    • Access Flag:类访问标志在常量池后面,标识类和接口的访问信息,如该Class文件是类还是接口,是否为public,是否为abstract等
    • Class :类索引,包含当前类的索引(this_class)父类索引(super_class),接口索引(interfaces),通过这个索引,我们可以去常量池找这个类的全限定描述符
    • fields:字段表集合,记录了类中每个变量的变量名, 变量类型, 访问标识, 属性等
    • method:方法表集合,方法表和字段表的结构比较类似,包含了访问标识,名称索引,描述符索引,属性表索引等信息
    • attributes:属性表,属性表非常庞大,包含方法的字节码指令,方法表里面的属性表索引就是指向该方法的字节码指令,常量值,方法抛出的异常等数据
    Dex字节码

    说完了class字节码,接下来对比说一下Dex字节码文件,我们知道class字节码文件只能在JVM上面运行,无法在Android虚拟机上运行,只有dex文件才能在Android虚拟机上运行,那么dex文件又是什么呢?它和class文件的区别是什么呢?

    Android项目通过gradle构建生成apk文件,apk文件就是Android的安装包,安装包主要由dex文件,so文件,资源文件,manifest文件组成,如果有使用kotlin的话,apk包里面还会有kotlin的编译产物。
    在这里插入图片描述

    我这里只讲dex文件,Android的编译器会将java文件编译成dex,编译流程如下:

    SourceCode(.java) — javac → Java Bytecode(.class) — Proguard → Optimized Java bytecode(.class) — Dex → Dalvik Optimized Bytecode(.dex)

    从上面的流程看到,编译器第一步同样是将java文件转换成了class字节码文件,之后便是Android编译器所特有的部分:

    • Proguard流程会对字节码文件进行压缩,优化和混淆,我们可以在gradle中开启配置proguradFiles的规则来开启我们的Proguard流程
    • 当Proguard优化字节码文件后,dx编译器(AndroidStudio3.0之后开始采用D8编译器)会将优化后的字节码文件生成dex文件。

    java8中引入了lambda等一些语法糖新特性,所以为了兼容这些语法糖,Android编译器在编译的途中会经历拖糖的操作,在Android Gradle Plugin3.1版本之前是用的第三方的插件进行脱糖操作,将所有的流程串起来,它的步骤如下图:
    在这里插入图片描述
    我们接着看一下dex文件的文件结构

    • Header:dex文件的头文件同样包含了magic魔数,用来标识是否是dex文件,还包含了checksum和signature等文件校验和签名信息码,file_size,header_size文件和头大小以及其他数据的大小等信息等等
    • String_ids:字符串偏移数组,表示每个字符串在 data 区的偏移量,根据偏移量在Data区拿到数据
    • Type_ids:数据类型索引,表示所有引用的数据类型在字符串中的索引
    • Protos_ids:方法声明索引
    • Fields:记录了所属类,类型以及方法名
    • Methods:方法表
    • Classes:类信息索引,记录了类信息,包括接口,超类,类数据偏移量
    • Data:数据区,保存了dex文件中所有类的数据

    dex的文件和class文件存放的数据是一样的,只是结构会有些不一致,而且dex文件是多个class文件的集合,所有会有数据去重,重排列等优化处理处理。

    我们接着来看看虚拟机的第二个能力,如何装载上面的字节码文件

    装载字节码文件

    class字节码文件

    java编译器将源文件编译成class字节码文件后,jvm就直接可以运行了,但想要运行,首先要将这个字节码文件加载进内存,jvm通过ClassLoader来加载指定路径的字节码文件,字节码的文件可以通过网络下载,也可以通过本地读取。我们看一下ClassLoader类加载class的实现。

    //java.lang.ClassLoader
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            synchronized (getClassLoadingLock(name)) {          
            //查找.class是否被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //通过父类或者根加载器加载,双亲委派模型的实现
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                        } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   
                }
                    if (c == null) {                 
                    //找到根加载器依然为空,只能子加载器自己加载了
                    long t1 = System.nanoTime();
                        c = findClass(name);
                }
            }
            // 解析class文件
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    通过上面的代码可以看到,jvm加载class是通过双亲委派模型加载,也就是会先采用父类或者根加载器来加载这个class文件,如果父类后者根类没法加载,才使用子类加载,加载方法为findClass()。

    通过双亲委派来加载class字节码,这样可以避免类的重复加载以及安全性问题,如果我们要破坏双亲委派,则直接重写整个loadClass方法,如果遵循双亲委派模型,只需要实现findClass方法方法就行了。

    jvm的主要几个类加载器,BootstrapClassloader;ExtentionClassLoader;ApplicationClassloader都是通过复写findClass去加载指定路径的class文件。Android虚拟机也是通过BaseDexClassLoader复写这个方法去DexList里面寻找Dex里面指定的class数据。

    当JVM读取到字节码的二进制文件到内存后,会开始解析,读取头进行校验,在堆中创建运行时常量池和方法区,将字节码文件中的常量池,方法区和其他数据读取到运行时常量池和方法区的数据结构中。上面已经介绍了加载的过程,我们接着看看链接的过程。

    加载和链接是同步进行的,链接主要是校验,准备,解析这三步。

    • 校验就是验证魔数,版本号等信息,元数据,符号引用等是否正确
    • 准备主要是为变量分配内存并设置初始值
    • 解析是将常量池中的符号引用替换成直接引用,如指针,偏移量等。

    链接完成后就是初始化,初始化主要是执行初始化静态语句块和变量赋值的操作。初始化完成后,就会返回一个可以使用的class对象了。

    我们总结一下字节码文件加载,链接和初始化的过程。

    • 加载(loadClass,findClass)
      • 将字节码数据加载进内存,将字节码中的常量池方法区等静态数据转换成运行时常量池和方法区运行时数据结构,并在堆中生成Class对象
    • 链接(ResoveClass)
      • 验证:保证加载类的信息符合JVM规范,没有安全方面的问题
      • 准备:为变量分配内存,并设置初始值
      • 解析:虚拟机常量池的符号引用替换为直接引用(目标对象的指针,偏移,句柄等)
    • 初始化
      • static静态代码块
    dex字节码文件

    Android虚拟机怎么执行dex文件呢?他其实和java是一样的,读取字节码二进制字节流,然后进行加载,链接和初始化的过程,相同的部分就不说了,主要说说Android虚拟机不同的部分,也就是字节码文件加载的这部分,我们看一下Android虚拟机用来加载字节码的BaseDexClassLoader实现。

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    

    可以看到,Android的BaseDexClassLoader会在构造函数中会根据dex文件的路径,将dex文件读取到DexPathList中,并重写类的加载方法findClass,在pathList中去寻找想要加载的字节码文件,然后进行加载,链接,初始化操作后,返回一个Class对象。

    这里我用QQ空间热修复方案常用的一张图,pathList就是一个Dex文件的数组,findClass会从前往后查找匹配的字节码文件,这也是热修复的原理之一,将要修复的字节码文件插入到pathList的dex数组前面,那么加载字节码的时候就会首先加载我们插入的字节码文件,达到热修复的目的。不过热修复的难点主要还要在于解决安全校验问题,这里就不说了。

    在这里插入图片描述

    虽然Android虚拟机运行的是dex文件,但是并不会直接执行apk里面的dex文件,apk文件只是一个安装包,当我们运行这个安装包时,Android系统会将dex文件进行优化生成odex文件,我们的启动程序加载的dex文件其实就是这个odex文件。Davlik虚拟机通过dexopt方法来优化dex文件,优化的过程包括校验,方法内联,指令优化等,最终生成的odex文件依然还是字节码文件。

    但是Art虚拟机就不一样了,我们知道,java之所以比C运行慢,主要的原因是因为jvm执行的字节码文件,字节码文件是中间文件,而C直接运行就是机器码文件,c的编译器一开始就会将源代码编译成机器码文件,也就是AOT编译。为了让程序运行的更快,所以ART虚拟机也引入了AOT编译技术,通过dex2oat方法,直接会将dex文件编译成机器码,虽然编译后的文件还是以odex结尾,但是这个odex文件和dalvlk优化后生成的odex是不一样的,ART优化后的odex文件其实是一个ELF文件,ELF文件是linux的一种文件格式,里面包含了该dex的机器码数据,还有dex文件。但是dex2oat的耗时很久,也很占空间,导致安装耗时很久,对低性能手机很不友好,所以现在ART也不会在安装的时候就进行字节码编译成机器码的操作,而是运行时,对热代码在后台进行编译操作,或者通过运行时编译为机器码,也就是JIT技术,一般运行七八次,就能将该应用的所有热代码编译成机器码文件。

    可以看到,JS引擎的WebAssembly其实也是AOT的技术,让引擎能够直接运行机器码,和ART在理论上是异曲同工的,同样也是为了优化字节码运行慢而引入的一种优化技术。

    接下来就是虚拟机内存管理的部分了,我们接着往下看。

    内存管理

    运行时内存区域

    在了解Java和Android虚拟机运行时内存区域之前,我们先了解一下操作系统进程的内存区域,虚拟机只是系统中的一个进程,所以我们可以从更大的视野看看一个进程在运行时的内存是怎样的,这里我以Linux系统为例。

    Linxu进程的内存分为用户空间和内核空间两部分,内核空间就是系统的运行空间,用户空间是进程的运行空间,Linux进程的用户空间分布如下
    在这里插入图片描述

    • text段:用于存放程序代码的区域
    • data段:用来存放编译阶段就能确定的数据
    • bss段:用来存放未初始化的全局变量

    当JVM运行在Linux系统时,作为Linux系统的一个进程,他同样具有上面同样的内存区域,但是在JVM运行字节码文件时,又将堆内存做了细分。

    在这里插入图片描述

    可以看到,JVM将堆分为了永久代,新生代和老年代,永久代其实就是运行时常量池和方法区,因为这部分的内存几乎不会被回收,所以称为永久代,新生代和老年代用来存放对象,当经过几次垃圾回收后依然存活的对象就从新生代进入了老年代。

    我们再来看一下Android虚拟机的内存分布,Android虚拟机将堆内存同样分为三个区域:年轻代,年老代,永久代,针对年轻代和老年代,ART和Dalvik又做了细分,主要可以分为下面几种

    在这里插入图片描述

    我们具体看一下这几种堆的作用

    • ZygoteSpace:Zygote进程启动过程中创建的所有对象。这些对象是所有进程共享
    • ImageSpcace:存放预加载的类,Android Framework中通用的类都都是存储在这里
    • Large Obj Space:存放大于12k的类对象的空间
    • Main Allooc Space:存放小对象的空间
    • Non Moving Space/Linear Alloc:只读的线性内存空间,主要用来存储虚拟机中在进程生命周期都不会结束清理的永久数据的类对象。

    为什么Android的虚拟机要对堆划分这么多区域呢?主要都是为了性能的考虑,ZygoteSpace和ImageSpace存放共享的预加载的类,这样可以提高启动速度,还有根据对象的大小和特性划分LargeObjSpace,AllocSpace和Non Moving Space可以采用不同的垃圾回收策略,提高gc的效率和性能。

    我们接着来看看虚拟机的垃圾回收机制

    垃圾回收机制

    垃圾回收机制分为对象存活判断和垃圾回收两部分,对象存活判断主要有下面两种方法

    1. 引用计数:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,计数器为0的对象就是不可能再被使用的。
    2. 可达性分析:通过判断对象是否被GCROOT引入来判断对象是否还能被使用,GCROOT包括局部变量表里面的变量和引用,静态变量和全局变量,JNI方法。

    垃圾清除算法主要是下面三种

    1. 标记清除:标记清除通过扫描一次对象堆,标记出需要清除的对象,然后进行清除操作,整个过程需要将整个程序暂停,清除完成之后才恢复程序运行,而且这个算法会带来碎片化的问题。
    2. 复制算法:复制算法会将存活的对象复制到一块内存,然后将遗留下来的对象进行清理,这种算法不会产生碎片问题,但是会占用更多的内存,因为要一块空间来复制存活的对象。
    3. 标记整理:扫描一遍一次对象堆,标记处需要清除和存活的对象,然后将存活的对象全部在内存中向前移动,需要清除的对象自然就会在排到内存的后面,然后进行清楚。

    不管是JVM虚拟机,还是Android的虚拟机,垃圾清除算法都是在上面三种中进行改进和优化。比如Dalvik的垃圾清除算法主要是标记清理,这样GC时会造成程序卡顿,ART改进了垃圾回收机制,除了根据对象大小和特性,开辟了更多的内存区域,同时在调用标记清楚算法时,只需要在回收时暂停一次程序,标记操作不需要暂停,而是让线程自己标记。在清楚时,也会更加高效。

    接下来就是虚拟机最后一块能力了,也就是指令的执行

    指令解析和执行

    在前面说到过,每个方法里面的字节码指令会存放在Attributes属性表里,那么虚拟机如何执行方法的Code呢?我们先看看下面这个简单的函数被编译成字节码后的形式

    public static int addAndDouble(int a, int b){
        return (a + b) * 2;
    }
    

    在这里插入图片描述
    我来详细讲一下这段指令的过程,iload_0,iload_1表示加载局部变量表中偏移为0和1的变量,也就是a和b这两个变量,iadd表示相加,iconst_2表示2,imul表示有符号乘法。Ireturn表示返回int类型。虚拟机的执行器通过解释这些指令,就将我们的方法运行起来了。字节码指令非常多,jvm理论上最多支持256条指令,这里介绍一些主要的指令

    • 加载和存储指令:iload,lload,fload……
    • 运算指令:加法(iadd,ladd,fadd,dadd),减法(isub,lsub,fsub,dsub),乘法,除法,求余,取反……
    • 类型转换:i2b,i2c,i2s,……
    • 操作数栈管理指令:pop
    • 方法调用和返回指令:invokevirtural,ireturn
    • 对象创建和访问:new,newarray,getfield,bastore,baload……
    • 异常处理:athrow
    • 同步指令:monitorenter,moniterexit
    • 控制转移指令:goto

    那么jvm是如何执行这些指令的呢?
    jvm每调用一个方法,都会有一个栈帧来支持这个方法的调用,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。栈帧结构如下图。
    在这里插入图片描述

    我们在回到上面的函数,它在栈帧中的操作如下。

    在这里插入图片描述

    可以看到,JVM执行指令是基于栈的,JVM的执行引擎,本质上就是一段switch函数,这段swtich函数执行到对应的指令时,便会基于栈来操作指令,其实这也是JVM运行慢的原因,他是基于栈的解释执行模型。

    Android的指令是基于寄存器的,这种设计需要硬件支持,手机的Arm架构是支持这样的特性的。dex方法的字节码指令不会放在栈里面,而是放在寄存器里面,基于寄存器的指令数量更少,运行速度会更快,由于需要硬件支持,所以跨平台不友好。

    我们可以看一段ART是如何基于解释器来执行字节码文件的代码实现

    template<bool do_access_check, bool transaction_active>
    JValue ExecuteSwitchImpl(Thread* self, const DexFile::CodeItem* code_item,
                             ShadowFrame& shadow_frame, JValue result_register,
                             bool interpret_one_instruction) {
      constexpr bool do_assignability_check = do_access_check;
      self->VerifyStack();
     
      uint32_t dex_pc = shadow_frame.GetDexPC();
      const auto* const instrumentation = Runtime::Current()->GetInstrumentation();
      const uint16_t* const insns = code_item->insns_;
      const Instruction* inst = Instruction::At(insns + dex_pc);
      uint16_t inst_data;
      ArtMethod* method = shadow_frame.GetMethod();
      jit::Jit* jit = Runtime::Current()->GetJit();
     
      // TODO: collapse capture-variable+create-lambda into one opcode, then we won't need
      // to keep this live for the scope of the entire function call.
      std::unique_ptr<lambda::ClosureBuilder> lambda_closure_builder;
      size_t lambda_captured_variable_index = 0;
      do {
        dex_pc = inst->GetDexPc(insns);
        shadow_frame.SetDexPC(dex_pc);
        TraceExecution(shadow_frame, inst, dex_pc);
        inst_data = inst->Fetch16(0);
        switch (inst->Opcode(inst_data)) {
          case Instruction::NOP:
            PREAMBLE();
            inst = inst->Next_1xx();
            break;
          case Instruction::MOVE:
            PREAMBLE();
            shadow_frame.SetVReg(inst->VRegA_12x(inst_data),
                                 shadow_frame.GetVReg(inst->VRegB_12x(inst_data)));
            inst = inst->Next_1xx();
            break;
    ......
         }
      } while (!interpret_one_instruction);
      // Record where we stopped.
      shadow_frame.SetDexPC(inst->GetDexPc(insns));
      return result_register;
    }
    

    可以看到上面的代码便是通过switch函数,来执行对应的字节码指令。

    上面介绍的是虚拟机基于解析字节码指令来执行方法,其实虚拟机还有一种或方法能执行我们的代码,就是直接运行机器码文件。如ART虚拟机引入的AOT,就是提前将字节码编译成机器码,这样ART虚拟机就可以直接运行机器码,而不需要解释执行字节码文件,JIT也是在运行过程中,将热代码编译成机器码后运行。直接运行机器码的过程这里就不详说了。我们通过下图可以一览JVM运行字节码文件的全流程。

    在这里插入图片描述

    至此,虚拟机的知识已经讲完了,我们再来总结一下一个虚拟机所拥有的模块和功能。

    基于字节码的编译模块:该模块主要是对源代码进行词法,语法,语义分析生成AST,并将AST生成中间文件,jvm的编译模块是javac,Android 虚拟机的编译模块是javac和dx或d8,v8的编译模块是Parser和Ignition

    • 加载器模块:加载字节码的二进制流,并解析映射至堆内存中
    • 解释器模块:解释执行函数字节码指令,虚拟机通过解释执行字节码的一种方式。
    • 基于机器码的编译器模块:是虚拟机执行字节码的另一种方式,将字节码转化为本地机器代码后执行,如JIT和AOT,V8的TurboFan

    真正想要深入了解虚拟机的方式就是自己动手写一个虚拟机,我们可以用Python手写一个虚拟机,因为Python已经有了内存回收的模块,我们只需要写一个类的加载模块和解释器模块就可以了。这里只是简单的介绍一下一个虚拟机具备的基本能力,有了这些基本能力,我们也有了深入了解虚拟机或者对虚拟机进行优化的理论知识。

    比如华为的方舟编译,也是一个虚拟机,既然是虚拟机,那么就逃不开上面的模块,所以它之所以快,或许是用到了AOT,或者是对堆内存有了更多的细分,根据场景采用了更合适的垃圾回收算法。

    基于上面讲的部分,我们同样可以迁移到对其他的虚拟机的学习中,比如我们可以去学习V8是怎么进行垃圾回收的,V8是怎么解释执行字节码的,V8是怎么加载类文件的。

    最后,为个人的技术公众号打个广告,只写高质量的技术文章,欢迎关注
    在这里插入图片描述

    展开全文
  • 从系统内部全面深入的讲解了android系统及android应用程序的整个运行流程。
  • Android虚拟机

    千次阅读 2018-01-23 23:07:06
    ... Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格

    转载自:https://www.cnblogs.com/lao-liang/p/5111399.html


    1 什么是Dalvik虚拟机

      Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁、节省资源的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层。

    2 Dalvik虚拟机的功能

      Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。Dalvik充分利用Linux进程管理的特定,对其进行了面向对象的设计,使得可以同时运行多个进程,而传统的Java程序通常只能运行一个进程,这也是为什么Android不采用JVM的原因。Dalvik为了达到优化的目的,底层的操作大多和系统内核相关,或者直接调用内核接口。另外,Dalvik早期并没有JIT编译器,直到Android2.2才加入了对JIT的技术支持。

    3 Dalvik虚拟机和Java虚拟机的区别

      本质上,Dalvik也是一个Java虚拟机。但它特别之处在于没有使用JVM规范。大多数Java虚拟机都是基于栈的结构(详情请参考:理解Java虚拟机体系结构),而Dalvik虚拟机则是基于寄存器。基于栈的指令很紧凑,例如,Java虚拟机使用的指令只占一个字节,因而称为字节码。基于寄存器的指令由于需要指定源地址和目标地址,因此需要占用更多的指令空间。Dalvik虚拟机的某些指令需要占用两个字节。基于栈和基于寄存器的指令集各有优劣,一般而言,执行同样的功能,前者需要更多的指令(主要是load和store指令),而后者需要更多的指令空间。需要更多指令意味着要多占用CPU时间,而需要更多指令空间意味着数据缓冲(d-cache)更易失效。更多讨论,虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 给出了非常详细的参考。

      Java虚拟机运行的是Java字节码,而Dalvik虚拟机运行的是专有文件格式dex。在Java程序中,Java类会被编译成一个或多个class文件,然后打包到jar文件中,接着Java虚拟机会从相应的class文件和jar文件中获取对应的字节码。Android应用虽然也使用Java语言,但是在编译成class文件后,还会通过DEX工具将所有的class文件转换成一个dex文件,Dalvik虚拟机再从中读取指令和数据。dex文件除了减少整体的文件尺寸和I/O操作次数,也提高了类的查找速度。

      由下图可以看到,jar和apk文件的组成结构,以及class文件和dex文件的差异。dex格式文件使用共享的、特定类型的常量池机制来节省内存。常量池存储类中的所有字面常量,它包括字符串常量、字段常量等值。

      总的来说,Dalvik虚拟机具有以下特点:

    • 使用dex格式的字节码,不兼容Java字节码格式
    • 代码密度小,运行效率高,节省资源
    • 常量池只使用32位的索引
    • 有内存限制
    • 默认栈大小是12KB(3个页,每页4KB)
    • 堆默认启动大小为2MB,默认最大值为16MB
    • 堆支持的最小启动大小为1MB,支持的最大值为1024MB
    • 堆和栈参数可以通过-Xms和-Xmx修改

    4 Dalvik系统结构

      实际上,Dalvik是基于Apache Harmony(Apache软件基金会的Java SE项目)的部分实现,提供了自己的一套库,即上层Java应用程序编写所使用的API。

      以上图示来自tech-insider。Apache Harmony大体上分为三个层:操作系统、Java虚拟机、Java类库。它的特点在于虚拟机和类库内部被高度模块化,每一个模块都有一定的接口定义。操作系统层与虚拟机层之间的接口由Portability Layer定义,它封装了不同操作系统的差异,为虚拟机和类库的本地代码提供了一套统一的API访问底层系统调用。虚拟机与类库之间的接口除了Java规范定义的JNI、JVMITI外,还加入了一层虚拟机接口,由内核类和本地代码组成。实现了虚拟机接口的虚拟机都可以使用Harmony的类库实现,并且可以被Harmony提供的同一个Java启动程序启动。

      下面是Dalvik虚拟机的结构图:

     

      一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。

    4.1 dex文件结构

      dex文件结构和class文件结构差异的地方很多,但从携带的信息上看,dex和class文件是一致的。

    • header:存储了各个数据类型的起始地址、偏移量等信息。
    • proto_ids:描述函数原型信息,包括返回值,参数信息。比如“test:()V”
    • methods_ids:函数信息,包括所属类及对应的proto信息。

      更多dex格式的内容,Android安全–Dex文件格式详解 这篇文章进行了非常详细的介绍。虽然dex文件的结构很紧凑,但想要运行时的性能得到进一步提升,还需要对dex文件进行进一步优化。优化主要针对以下几个方面:

    • 调整所有字段的字节序和对齐结构中的每一个域
    • 验证dex文件中的所有类
    • 对一些特定的类进行优化,对方法里的操作码进行优化

      dex文件经过优化后文件大小会膨胀,大约增加到原来的1~4倍。对于内置应用,一般在系统编译后,便会生成优化文件(odex: Optimized dex)。一个Android应用程序,需要经过以下过程才可以在Dalvik虚拟机上运行:

    • 把Java源文件编译成class文件
    • 使用DX工具把class文件转换成dex文件
    • 使用aapt工具把dex文件、资源文件以及AndroidManifest.xml文件(二进制格式)组合成APK
    • 将APK安装到Android设备运行

      上图(来自网络)详尽地展示了最终签名后的APK是怎么来的。

    4.2 Dalvik类加载器

      一个dex文件需要类加载器加载原生类和Java类,然后通过解释器根据指令集对Dalvik字节码进行解释和执行。Dalvik类加载器使用mmap函数,将dex文件映射到内存中,通过普通的内存读取操作即可访问dex文件,然后解析dex文件内容并加载其中的类到哈希表中。

    4.2.1 解析dex

      总的来说,dex文件可以抽象为三个部分:头部、索引、数据。通过头部可以知道索引的位置和数目,以及数据区的起始位置。将dex文件映射到内存后,Dalvik会调用dexFileParse函数对其进行分析,分析的结果放到DexFile数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向class索引的起始位置。为了加快class的查找速度,还创建一个哈希表,对class名字进行哈希并生成索引。

    4.2.2 加载class

      解析工作完成后就进行class的加载,加载的类需要用ClassObject数据结构来存储。

    typedef struct Object {
        ClassObject* clazz;  // 类型对象
        Lock lock;           // 锁对象
    } Object;

      其中clazz指向ClassObject对象,还包含一个Lock对象。如果其它线程想要获取它的锁,只有等这个线程释放。Dalvik每加载一个class都会对应一个ClassObject对象,加载过程会在内存中分配几个区域,分别存放directMethod, virtualMethod, sfield, ifield。这些信息从dex文件的数据区中读取。字段Field的定义如下:

    复制代码
    struct Field {
        ClassObject* clazz;    //所属类型
        const char* name;      // 变量名称
        const char* signature; // 如“Landroid/os/Debug;”
        u4 accessFlags;        // 访问标记
        
        #ifdef PROFILE_FIELD_ACCESS
            u4 gets;
            u4 puts;
        #endif
    };
    复制代码

      待得到class索引后,实际的加载由loadClassFromDex来完成。首先它会读取class的具体数据,分别加载directMethod, virtualMethod, ifield和sfield,然后为ClassObject数据结构分配内存,并读取dex文件的相关信息。加载完成后,将加载的class通过dvmAddClassToHash函数放入哈希表,以方便下次查找;最后,通过dvmLinkClass查找该类的超类,如果有接口类则加载相应的接口类。

    4.3 Dalvik解释器

      对于任何虚拟机来说,解释器无疑是核心的部分,所有的Java字节码都经过解释器解释执行。由于Dalvik解释器的效率很重要,Android分别实现了C语言版和各种汇编语言版的解释器。解释器通常是循环执行,需要一个入口函数调用处理程序执行第一条指令,而后每条指令执行时引出下一条指令,通过函数指针调用处理程序。

    4.4 内存管理

      垃圾收集是Dalvik虚拟机内存管理的核心。此处只介绍Dalvik虚拟机的垃圾收集功能。垃圾收集的性能在很大程度上影响了一个Java程序内存使用的效率。Dalvik虚拟机使用常用的Mark-Sweep算法,该算法分Mark阶段(标记出活动对象)、Sweep阶段(回收垃圾内存)和可选的Compact阶段(减少堆中的碎片)。Android内存管理原理  这篇文章讲解得很详细。

      垃圾收集的第一步是标记出活动对象,因为没有办法识别那些不可访问的对象,这样所有未被标记的对象就是可以回收的垃圾。当进行垃圾收集时,需要停止Dalvik虚拟机的运行(除垃圾收集外),因此垃圾收集又被称作STW(stop-the-world)。Dalvik虚拟机在运行过程中要维护一些状态信息,这些信息包括:每个线程所保存的寄存器、Java类中的静态字段、局部和全局的JNI引用,JVM中的所有函数调用会对应一个相应C的栈帧。每一个栈帧里可能包含对对象的引用,比如包含对象引用的局部变量和参数。所有这些引用信息被加入到一个根集合中,然后从根集合开始,递归查找可以从根集合出发访问的对象。因此,Mark过程又叫做追踪,追踪所有可被访问的对象。

      垃圾收集的第二步就是回收内存。在Mark阶段通过markBits位图可以得到所有可访问的对象集合,而liveBits位图表示所有已经分配的对象集合。通过比较liveBits位图和markBits位图的差异就是所有可回收的对象集合。Sweep阶段调用free来释放这些内存给堆。

      在底层内存实现上,Android系统使用的是msspace,这是一个轻量级的malloc实现。除了创建和初始化用于存储普通Java对象的内存堆,Android还创建三个额外的内存堆:

    • "livebits"(用来存放堆上内存被占用情况的位图索引)
    • "markbits"(在GC时用于标注存活对象的位图索引)
    • “markstack”(在GC中遍历存活对象引用的标注栈)

      虚拟机通过一个名为gHs的全局HeapSource变量来操控GC内存堆,而HeapSource里通过heaps数组可以管理多个堆(Heap),以满足动态调整GC内存堆大小的要求。另外HeapSource里还维护一个名为"livebits"的位图索引,以跟踪各个堆(Heap)的内存使用情况。剩下两个数据结构"markstack"和"markbits"都是用在垃圾回收阶段。

      上图中"livebits"维护堆上已用的内存信息,而"markbits"这个位图索引则指向存活的对象。 A、C、F、G、H对象需要保留,因此"markbits"分别指向他们(最后的H对象尚在标注过程中,因此没有指针指向它)。而"markstack"就是在标注过程中跟踪当前需要处理的对象要用到的标志栈,此时其保存了正在处理的对象F、G和H。

    4.5 Dalvik的启动流程

      Dalvik进程管理是依赖于linux的进程体系结构的,如要为应用程序创建一个进程,它会使用linux的fork机制来复制一个进程。Zygote是一个虚拟机进程,同时也是一个虚拟机实例的孵化器,它通过init进程启动。之前的文章有对此过程有详细介绍:Android系统启动分析(Init->Zygote->SystemServer->Home activity)。此处分析Dalvik虚拟机启动的相关过程。

       AndroidRuntime类主要做了以下几件事情:

    • 调用startVM创建一个Dalvik虚拟机,JNI_CreateJavaVM真正创建并初始化虚拟机实例
    • 调用startReg注册Android核心类的JNI方法
    • 通过Zygote进程进入Java层

       在JNI中,dvmCreateJNIEnv为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象。最后调用dvmStartup来初始化前面创建的Dalvik虚拟机实例。函数dvmInitZygote调用了系统的setpgid来设置当前进程,即Zygote进程的进程组ID。这一步完成后,Dalvik虚拟机的创建和初始化工作就完成了。

    5 Android的启动

    • 启动电源,加载引导程序到RAM
    • BootLoader引导
    • Linux Kernel启动
    • Init进程创建
    • Init fork出Zygote进程,Zygote进程创建虚拟机;创建系统服务
    • Android Home Launcher启动

     

     

    参考:

    《Android技术内幕》

    展开全文
  • Android虚拟机Dalvik完整源码

    热门讨论 2010-05-19 14:22:11
    Android虚拟机Dalvik完整源码,宝贵资源,欢迎下载! This directory contains the Dalvik virtual machine and core class library, as well as related tools, libraries, and tests. A note about the licenses...
  • ip地址不能设置为127.0.0.1, 手机上设置为电脑的ip地址,模拟器可以用android内置IP 10.0.2.2 后来发现这么连接也没有用,遂查找了其他的方法,说换成自己的 IP 地址,修改之后果然有效! 接下来我将介绍如何在 ...

    最近在搞个小项目,想通过 WebService 从本地服务器的数据库获取数据, 建立 connection 的代码是这么写的:

    HttpGet httpget = new HttpGet("http://10.0.2.2/data.php"); 
    

    原因是网上说

    ip地址不能设置为127.0.0.1, 手机上设置为电脑的ip地址,模拟器可以用android内置IP 10.0.2.2

    后来发现这么连接也没有用,遂查找了其他的方法,说换成自己的 IP 地址,修改之后果然有效!

    接下来我将介绍如何在 Mac 上查看 IP 地址:

    1. 进入【系统偏好设置】菜单中,选择【网络】

    在这里插入图片描述

    2. 在【网络】菜单中,点击【高级】按钮

    在这里插入图片描述

    3. 在弹出的界面中点击【TCP/IP】标签,即可在下边找到【IPv4 地址】

    在这里插入图片描述

    展开全文
  • Dalvik虚拟机 在《Android性能:内存篇之虚拟机概论》中我们已经初步了解了JVM的结构基础与内存空间,,...内存篇之内存回收》对内存回收有了深入的了解,接下来我们详细聊聊Android虚拟机的内存回收机制及与虚拟机...

    Android性能:内存篇之Android虚拟机

    《Android性能:内存篇之虚拟机概论》中我们已经初步了解了JVM的结构基础与内存空间,,但是Android系统中的java虚拟机毕竟不是使用JVM,而是Delvik(Android系统5.0之前版本)与ART(Android4.4及之后版本),在《Android性能:内存篇之内存回收》对内存回收有了深入的了解,接下来我们详细聊聊Android虚拟机的内存回收机制及与虚拟机之间的关联。

    一、Dalvik虚拟机

    1. Delvik

    Dalvik是Google公司自己设计用于Android平台的Java虚拟机,但它运行的不是 .class文件(java字节码),而是.dex文件(dex字节码)。 Dalvik虚拟机包含有一个解释器,用来执行dex字节码,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。

    主流的大部分Davik采取的都是标注与清理(Mark and Sweep)回收算法,也有实现了拷贝GC算法的(具体算法看《Android性能:内存篇之内存回收》)。

    2. 内存分配

    Delvik中,内存分配实际上是对堆的分配和释放。当一个 Android 程序启动,应用进程都是从一个叫做 Zygote 的进程衍生出来,系统启动 Zygote 进程后,为了启动一个新的应用程序进程,系统会衍生 Zygote 进程生成一个新的进程,然后在新的进程中加载并运行应用程序的代码。其中,大多数的 RAM pages 被用来分配给Framework 代码,同时促使 RAM 资源能够在应用所有进程之间共享。

    为了整个系统的内存控制需要,系统会为每一个应用程序都设置一个堆的限制阈值,整个阈值在不同设备上会因为 RAM 大小不同而有所差异。如果应用占用内存空间已经接近整个阈值时,再尝试分配内存的话,就很容易引起内存溢出的错误。

    关于堆的大小,有三个重要数值:Java堆的起始大小(Starting Size)、最大值(Maximum Size)和增长上限值(Growth Limit)。

    -dalvik.vm.heapstartsize        
        
    -dalvik.vm.heapgrowthlimit       
    
    -dalvik.vm.heapsize 
    
    • Starting Size : 堆的起始大小,Dalvik虚拟机启动的时候,会先分配一块初始的堆内存给虚拟机使用,这个数值越大,应用启动越流畅,但是系统RAM也消耗越快(一些较大的应用需要扩张这个堆,从而引发GC和堆调整的策略,因此也会使应用反应更慢)。
    • Growth Limit:受控情况下的极限堆(仅仅针对dalvik堆,不包括native堆)大小,是系统给每一个程序的最大堆上限,dvm heap是可增长的,但是正常情况下dvm heap的大小是不会超过dalvik.vm.heapgrowthlimit的值,超过这个上限,程序就会OOM。
    • Maximum Size:不受控情况下的最大堆内存大小,起始就是我们在用largeheap属性的时候,可以从系统获取的最大堆大小,这个就是堆的最大值。不管它是不是受控的。这个值会影响非受控应用的dalvikheap size。一旦dalvik heap size超过这个值,直接引发oom。

    同时除了上面的这个三个指标外,还有几个指标也是值得我们关注的,那就是堆最小空闲值(Min Free)、堆最大空闲值(Max Free)和堆目标利用率(Target Utilization)。

    当我们尝试手动去生成一些几百K的对象,试图去扩大可用堆大小的时候,会导致频繁的GC,因为这些对象的分配会导致GC,而GC后会让堆内存回到合适的比例,而我们使用的局部变量很快会被回收理论上存活对象还是那么多,我们的堆大小也会缩减回来无法达到扩充的目的。

    3. 对象的分配和GC的步骤
    在这里插入图片描述
    大概步骤是:1. 分配内存;2.内存分配失败,则执行GC(但是不回收Soft的引用);3.分配内存;4.不够?扩大Growth Limit,再次分配内存;5.还不够?指定堆大小到最大,继续GC(释放Soft的引用);6.OOM之前再释放一次,实在不行就OOM吧

    二、ART虚拟机

    1. ART

    ART(Android Runtime)是 Android 上的应用和部分系统服务使用的托管式运行时,4.4开始使用,从Android 5.0(Lollipop)开始,ART就彻底代替了原先的Dalvik,成为Android系统上新的虚拟机,作为运行时的 ART 可执行 Dalvik 可执行文件并遵循 Dex 字节码规范。
    Art在GC上不像Dalvik仅有一种回收算法,Art在不同的情况下会选择不同的回收算法,比如Alloc内存不够的时候会采用非并发GC,而在Alloc后发现内存达到一定阀值的时候又会触发并发GC。

    2. 内存分配

    ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,Image Space用来存在一些预加载的类, Zygote Space和Allocation Space与Dalvik虚拟机垃圾收集机制中的Zygote堆和Active堆的作用是一样的,Large Object Space就是一些离散地址的集合,用来分配一些大对象从而提高了GC的管理效率和整体性能。

    3. 对象的分配和GC的步骤

    与Delvik基本一致,看上图即可。

    三、JVM、Delvik、ART的比较

    JVMDelvik
    基于栈(需要更多指令,移植性好,性能更低)基于寄存器(需要更少指令,移植性稍差,性能更佳)
    基于不同的硬件提供统一的Java应用运行时环境采用预加载,并不提供统一的环境
    执行的是class文件执行的是dex文件(由class文件转化),占用空间更小
    *为简化翻译,常量池只使用32位索引
    DelvikART
    安装时是dex字节码,运行需要即时 (JIT1) 编译器翻译成机器码再运行采用了AOT2预编译技术,安装时直接编译成机器码(Android 7.0 中,ART组合使用了AOT和JIT)
    占用空间小,启动运行慢占用空间大,安装时间稍长,启动运行更快,节约CPU资源
    GC的停顿为2次GC的停顿为1次(并发GC)
    回收器的总GC时间略长回收器的总GC时间更短
    GC次数较少优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收

    总的来说,ART在GC上做的比Dalvik好太多了,不光是GC的效率,减少pause时间,而且还在内存分配上对大内存的有单独的分配区域,同时还能有算法在后台做内存整理,减少内存碎片。对于开发者来说ART下我们基本可以避免很多类似gc导致的卡顿问题了。另外根据谷歌自己的数据来看,Art相对Dalvik内存分配的效率提高了10倍,GC的效率提高了2-3倍。


    1. JIT:Just in Time,即时编译 ↩︎

    2. AOT:Ahead-Of-Time,预编译 ↩︎

    展开全文
  • android虚拟机非常深奥,这本书是市面上少有的深入解析android虚拟机的书籍,是android进阶不二的选择。
  • 理解Android虚拟机结构

    千次阅读 2018-06-18 10:51:52
    1 什么是Dalvik虚拟机 Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩...
  • 1.JAVA虚拟机运行的是JAVA字节码,Dalvik虚拟机运行的是Dalvik字节码 java虚拟机:JAVA->class文件 dalvik虚拟机:JAVA->class文件->Dalvik字节码->打包到dex中->DVM通过解释DEX文件来执行这些字节码...
  • Android虚拟机的安装

    万次阅读 2018-07-13 09:28:49
    Android虚拟机的安装完成Android环境搭建后。1. 将C:\ProgramData\Microsoft\AndroidSDK\25\tools\lib下的AVD Manager.exe和SDK Manager.exe复制到根目录下,看看能否双击打开。 2. 右击SDKManager.exe,使用管理员...
  • 使用Android 虚拟机运行

    千次阅读 2020-04-28 00:19:40
    有些人可能使用的不是安卓手机,所以导致不能使用实体手机运行项目,所以今天要讲的是如何使用Android 虚拟机运行(PS:运行最好使用安卓手机)。 第一步:修改环境变量,去环境变量里添加一个ANDROID_SDK_HOME值...
  • Android虚拟机和Java虚拟机的区别

    千次阅读 2019-03-13 11:59:49
    1、文件格式不同 架构不同 2、dex和odex的区别 dex文件格式可以减少整体文件尺寸,提高I/O操作的类查找速度。... odex是为了在运行过程中进一步提高性能,对dex文件的进一步优化。......
  • Android虚拟机使用摄像头(命令行)

    千次阅读 2020-12-11 19:57:21
    Android虚拟机使用摄像头(命令行) 前言:百度到很多以前的方法,比如更改avd.ini配置文件的。一看自己的配置文件中和他们的有挺大差异就没动。后来在Android官方文档找到如下方法 emulator -avd avd名 -camera-...
  • Android虚拟机连接网络

    千次阅读 2019-10-31 14:41:46
    点击查看
  • Android 虚拟机快捷键

    2017-10-24 23:58:17
    电脑安装虚拟机后,没有实体键,很多操作都需要依赖这些快捷键来实现,下载吧,保准可用!

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 115,594
精华内容 46,237
关键字:

android虚拟机