Android虚拟机和类加载机制

Android虚拟机

Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件

Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基
本寄存器的,而后者的指令集是基于堆栈的。

image-20240229192845249

那么什么是基于栈的虚拟机,什么又是基于寄存器的虚拟机?

基于栈的虚拟机

对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有
一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的
虚拟机通过操作数栈进行所有操作。

image-20240229193021707

字节码指令

image-20240229193147883

ICONST_1 : 将int类型常量1压入操作数
栈;
ISTORE 0 : 将栈顶int类型值存入局部变
量0;
IADD : 执行int类型的加法 ;

执行过程

image-20240229193330926

基于寄存器的虚拟机

基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。

image-20240229193708780

基于栈和寄存器的虚拟机对比

栈式VS寄存器式相比
指令条数栈式 > 寄存器式
代码尺寸栈式 < 寄存器式
移植性栈式优于寄存器式
指令优化栈式更不易优化
解释器执行速度栈式解释器速度稍慢
代码生成速度栈式简单
简易实现中的数据移动次数栈式移动次数多

和JVM版相比,可以发现Dalvik版程序的指令明显减少了,数据移动也明显减少了。

分类特点优点缺点
基于栈的虚拟机指令集主要操作的是栈。指令通常不直接操作存储器或寄存器,而是操作栈顶的元素指令集可以简单且与硬件无关访问局部变量和传递参数可能需要频繁的入栈和出栈操作,这可能影响性能
基于寄存器的虚拟机指令集主要操作的是寄存器。指令通常会指定操作数所在的寄存器可以减少访问局部变量和传递参数所需的指令数量,从而提高性能指令集可能更复杂,且与硬件更相关

ART与Dalvik

DVM也是实现了JVM规范的一个虚拟机,默认使用CMS垃圾回收器,但是与JVM运行Class字节码不同,DVM执行dex(Dalvik Executable Format)—专为Dalvik设计的一种压缩格式.Dex文件是很多.class文件处理压缩后的产物,最终可以在Android运行时环境执行.

从Android2.2版本开始,支持JIT即时编译(Just In Time).在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化.

而**ART(Android Runtim)**是在Android4.4中引入的一个开发者选项,也是Android5.0及更高版本的默认Android运行时.ART虚拟机执行的是本地机器码.Android的运行时从Davik虚拟替换成ART虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含APK的字节码文件.

ART和Dalvik都是运行Dex字节码的兼容运行时,因此针对Dalvik开发的应用也能在ART环境中运行

那么,ART虚拟机执行的本地机器码从哪里来?

dexopt和dex2oat

dexopt和dex2oat都是Android系统中用于优化和转换Dex文件的工具,它们在应用安装和运行过程中起着重要的作用。

dexopt

在Dalvik中虚拟机在加载一个dex文件时,对dex文件进行验证和优化的操作,其对dex文件的优化结果变成了odex(Optimized dex)文件,这个文件和dex文件很像,只是使用了一些优化操作码

dex2oat

ART 预先编译机制,在安装时对dex文件执行AOT提前编译操作,编译为OAT(实际上是ELF文件)可执行文件(机器码)

Davik下应用在安装的过程中,会执行一次优化,将dex字节码进行优化生成odex文件.而ART下应用的dex字节码能翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候.ART引入了预先编译机制(Ahead Of Time),在安装时,ART使用设备自带的dex2oat工具来编译应用,dex中的字节码被编译成本地机器码.

image-20240229195847764

Android N的运行方式

  1. 从Android 7.0(Nougat)开始,Android系统引入了一种新的运行方式,称为混合编译(Hybrid Compilation)。这种方式结合了之前Android Runtime (ART) 的Ahead-of-Time (AOT) 编译和Just-in-Time (JIT) 编译的优点。

  2. 在Android N及以后的版本中,应用在安装时不再进行全量的AOT编译,而是生成更小的OAT文件,这个过程称为快速安装。这样可以减少应用的安装时间,减少存储空间的占用,同时也减少了系统的启动时间。

  3. 然后,当应用运行时,Android系统会使用JIT编译器对热点代码进行JIT编译,即在运行时将字节码编译成机器码。这样可以提高应用的运行效率,因为JIT编译器可以根据运行时的信息进行更优的优化。

  4. 最后,当设备处于空闲状态时,Android系统会启动后台编译服务,对JIT编译过的代码进行AOT编译,生成更优化的机器码。这个过程称为后台编译,或者Profile-guided Compilation。这样可以进一步提高应用的运行效率,同时也减少了运行时的CPU和内存占用。

总的来说,Android N的运行方式结合了AOT编译和JIT编译的优点,既保证了应用的运行效率,又减少了系统的启动时间和存储空间的占用。

image-20240229200240449

类加载机制

概述

在Android系统中,类加载机制主要由三种类加载器实现:BootClassLoader、PathClassLoader和DexClassLoader。

  1. BootClassLoader:BootClassLoader是Android系统的启动类加载器,它负责加载Android系统的核心类库,例如android.*和java.*等包中的类。BootClassLoader在Android系统启动时由Zygote进程创建,它是所有ClassLoader的父加载器。BootClassLoader的加载路径是固定的,通常包括/system/framework等目录。
  2. PathClassLoader:PathClassLoader主要用于加载Android应用的主dex文件和系统类库。它是BootClassLoader的子类,它的父加载器是BootClassLoader。PathClassLoader不能加载文件系统上的任意位置的.dex文件或.apk文件。
  3. DexClassLoader:DexClassLoader可以加载文件系统上的任意位置的.dex文件、.jar文件、.apk文件和.zip文件(包含.dex文件)。它也是BootClassLoader的子类,它的父加载器是BootClassLoader。DexClassLoader通常用于实现插件化技术,可以在运行时动态加载和卸载代码。

这三个类加载器在加载类时,都会遵循双亲委派模型。当一个类加载器需要加载一个类时,它首先会请求其父类加载器来加载这个类,只有当父类加载器无法加载这个类时,它才会尝试自己加载这个类。

在Android 5.0及以后的版本中,由于引入了ART运行环境,类加载机制发生了一些变化。在应用安装时,dex2oat工具会将.dex文件编译成.oat文件,然后在运行时直接加载.oat文件。这样可以提高应用的运行效率,但需要更多的存储空间来存储.oat文件。

总的来说,Android中的类加载机制主要由BootClassLoader、PathClassLoader和DexClassLoader实现,它们可以加载Android系统的核心类库和应用的.dex文件。

image-20240229202124406

ClassLoader

任何一个Java程序都是由一个或多个class文件组成,在程序运行时,需要将class文件加载到JVM中才可以使用,负责加载这些class文件的就是Java的类加载机制.ClassLoader的作用简单来说就是加载class文件,提供给程序运行时使用.每个Class对象的内部都有一个classLoader字段来标识自己是由哪个ClassLoader加载的

class Class<T> {
...
private transient ClassLoader classLoader;
...
}

ClassLoader是一个抽象类,而它的具体实现类主要有:

  • BootClassLoader

    用于加载Android Frameword层class文件

  • PathClassLoader

    用于Android应用程序类加载器,可以加载指定的dex,以及jar,zip

    ,apk中的classes.dex

  • DexClassLoader

    用于加载指定的dex,以及jar,zip,apk中的classes.dex

image-20240229200948699

它们之间的关系是:

image-20240229203911798

PathClassLoaderDexClassLoader的共同父类是BaseDexClassLoader

// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

可以看到两者唯一的区别在于:创建DexClassLoader需要传递一个optimizedDirectory参数,并且会将其创建为File对象,而PathClassLoader则直接给到null,因此两者都可以加载指定的dex,以及jar

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

File dexOutputDir = context.getCOdeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex", dexOutputDir.getAbsolutePath(), null.getClassLoader());

其实optimizedDirectory参数就是dexopt的产出目录(odex).

PathClassLoader创建时,这个目录为null,就意味着不进行dexopt?

并不是,optimizedDirectory为null时的默认路径为:/data/dalvik-cache

在API26源码中,将DexCLassLoader的optimizedDirectory标记为了deprecated弃用,实现也变为了:

public DexClassLoader(String dexPath, String optimizedDirectory,
                     String librarySearchPath, ClassLoader parent) {
  super(dexPath, null, librarySearchPath, parent);
}

和PathClassLoader一摸一样了

双亲委托机制

在创建ClassLoader时需要接收一个ClassLoader parent参数,这个parent的目的就在于实现类加载的双亲委托机制.

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载.

protected Class<?> loadClass(String name, boolean resolve) throws
  ClassNotFoundExecption {
  
  // 检查class时候有被加载
  Class c = findLoadedClass(name);
  if (c == null) {
    long t0 = System.nanoTime();
    try {
      if (parent != null) {
        // 如果parent不为null,则调用parent的loadClass进行加载
        c = parent.loadClann(name, false);
      } else {
        // parent为null,则调用BootClassLoader进行加载
        c = findBootstrapClassOrNull(name);
      } 
    } catch (ClassNotFoundException e) {
      
    }
    
    if (c == null) {
      // 如果都找不到就自己查找
      long t1 = System.nanoTime();
      c = findClass(name);
    }
  }
  return c;
}

双亲委托机制的好处:

  1. 避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次.
  2. 安全性考虑,防止核心API库被随意篡改.

findClass

可以看到所有父ClassLoader无法加载Class时,则会调用自己的findClass方法,findClass在ClassLoader中的定义为:

protected Class<?> findClass(String name) throws ClassNotFoundException {
  throw new ClassNotFoundExecption(name);
}

其实任何ClassLoader子类,都可以重写classLoaderfindClass.一般如果你不想使用双亲委托,则重写loadClass修改其实现.而重写findClass则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class,而我们的PathClassLoader会自己负责加载MainActivity

这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的Activity.说明PathClassLoader并没有重写loadCLass,因此我们可以来看看PathClassLoader中的findClass是如何实现的

public class BaseDexClassLoader extends ClassLoader {

    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<>();
				// 查找特定的class
        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;
    }
}

实现非常简单,从pathList中查找class.继续查看DexPathList

public DexPathList(ClassLoader definingContext, String dexPath,
                  String librarySearchPath, File optimizedDirectory) {
  // ......
  // splitDexPath 实现为返回 List<File>.add(dexPath)
  // spiltDexPath 实现为返回 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
  this.dexElements = makeDexElements(spiltDexPath(dexPath), optimizedDirectory,
                                    suppressedExecptions, definingContext);
  // .....
}
public Class findClass(String name, List<Throwable> suppressed) {
  // 从element中获得Dex的 DexFile
  for (Element element : dexElements) {
    DexFile dex = element.dexFile;
    if (dex != null) {
      // 查找class
      Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
      if (clazz != null) {
        return clazz;
      }
    }
  }
  if (dexElementsSuppressedExceptions != null) {
    suppressed.addAll(Arrays.asList(dexElementSuppressedExceptions));
  }
  return null;
}

热修复

热修复(Hot-Fix)是一种在不需要重新安装应用的情况下修复应用程序错误的技术。在Android中,热修复通常通过动态加载技术实现,可以在应用运行时动态替换掉有问题的代码。

热修复的基本原理是使用DexClassLoader或PathClassLoader加载包含修复代码的.dex文件或.apk文件,然后通过反射或其他方式调用这些修复代码,从而替换掉有问题的代码。

PathClassLoader中存在一个Element数组,Element类中存在一个dexFile成员表示dex文件,即:APK中有X个dex,则Element数组就有X个元素

image-20240301082948138

PathClassLoader中的Element数组为:[patch.dex, classes.dex, classes2.dex]。如果存在Key.class位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得dexElements中的DexFile,查找到了Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。

因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建Element对象,然后将这个Element对象插入到我们程序的类加载器PathClassLoaderpathList中的dexElements数组头部。这样在加载出现Bug的class时会优先加载fix.dex中的修复类,从而解决Bug。

  • 19
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android Java虚拟机ART是一种全新的虚拟机,它是Android Lollipop操作系统中默认的运行时环境,相比于旧版的Dalvik虚拟机,它能够提供更好的性能和体验。 ART的最大特点是在使用前将字节码转换为机器码,这样可以在运行时减少解释和编译的时间,从而提高应用程序的响应速度。此外,在ART中也引入了一些新的技术,例如预编译、AOT和热编译等,这些都能够优化应用程序的启动速度和运行效率。 在深入研究ART之前,必须先了解Java虚拟机JVM)的基本概念和原理。JVM是一种运行Java程序的虚拟机,将Java源代码转换为字节码,再由虚拟机解释执行。同样地,ART也采用相同的原理来实现应用程序的运行,只不过它将字节码转换为机器码,从而提高了运行速度和效率。 因此,熟悉Java虚拟机和ART的工作原理,能够帮助开发者更好地理解和优化应用程序的性能。此外,对于一些需要高效运行的应用场景(例如游戏、图像处理等),ART也能够提供更好的运行环境,提高应用程序的稳定性和响应能力。 总之,深入理解Android Java虚拟机ART对于开发者来说非常重要,尤其是在需要优化应用程序性能和响应速度的情况下。只有深入了解ART的原理和特点,才能更好地应用它来提高应用程序的运行效率。 ### 回答2: Android Java虚拟机ART是Android系统中最新的运行时环境。相较于旧有的Dalvik虚拟机,ART采用预编译技术,将应用程序字节码一次性编译为本地机器码,提高了应用程序的运行效率和响应速度,同时也降低了资源消耗。因此,深入理解Android Java虚拟机ART对于Android开发者来说是非常必要的。 深入学习ART,我们需要了解其内部运作机制,包括Dex编译、ClassLoader、Garbage Collection等关键概念。ART采用了AOT和JIT两种编译方式,也采用了一些新的优化方法,如Profile Guided Optimization(PGO)、Image Space Estimation等,以提高应用程序的可执行性和启动时间。 ART的ClassLoader实现了一种高效的动态加载技术,它使得应用程序可以在运行时动态更新代码库、插件包等,从而大大扩展了应用程序的功能和灵活性。同时ART的ClassLoader也是构建Android虚拟化环境的基础,它可以从不同的应用程序中加载开放的类,并为每个应用程序提供一个独立的执行环境。 最后,ART的Garbage Collection机制实现了一种全新的分代收集算法,将耗费大量时间的垃圾回收操作分散到不同的虚拟机堆内,从而大幅度提高了应用程序的性能和响应速度。 总之,深入理解Android Java虚拟机ART对于Android开发者来说十分关键,它将为我们提供更为深入的开发思路和方法,使我们的应用程序更加高效,同时也为我们的Android应用程序开发添上浓墨重彩的一笔。 ### 回答3: Android Java虚拟机ART (Android Runtime)是安卓4.4系统及以上版本中的默认虚拟机。相比原先的Dalvik虚拟机,ART可实现更高的性能和更好的系统稳定性。 ART的核心思想是AOT( Ahead of Time)编译。它在应用程序安装的时候就将应用程序代码转换成本地机器指令并编译成机器代码,以C/C++库的形式保存在设备上。相比Dalvik,在应用程序的执行过程中省去了JIT编译的时间和运算,能够提高应用程序的运行速度。 除此之外,ART还有几个重要的特点: 1. 超低功耗:ART的AOT编译技术使得应用执行时可以直接使用本地机器指令,减少了CPU的时间浪费,使得应用程序的功耗更低。 2. 内存占用减少: ART允许应用程序在运行时进行类加载,实现更高效的内存管理。相比Dalvik虚拟机,ART在处理内存和垃圾回收时能够更好地利用系统资源,减少了应用程序所占用的内存。 3. 支持快速应用开发:通过使用ART虚拟机可以通过模块形式快速开发出具有更好体验的应用程序。 总之,深入理解Android Java虚拟机ART需要着重理解ART AOT编译原理、内存管理机制、以及对快速应用开发的支持。这些特点的综合优势使得安卓应用程序能够实现更快的运行速度、更低的功耗、更快的开发效率和更好的用户体验。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值