Android Runtime:ART与Dalvik设计哲学差异(2)

22 阅读7分钟

一、历史背景与设计驱动因素

1.1 移动设备硬件发展的挑战

在Android早期版本(2.2 - 4.4),移动设备普遍存在内存容量小(512MB以下)、CPU性能弱的特点。Dalvik针对这种资源受限环境设计,采用JIT即时编译技术以减少安装包体积和内存占用。随着硬件性能提升(多核CPU、大容量内存普及),用户对应用流畅度和响应速度要求提高,促使Android 5.0引入ART运行时,通过AOT预编译技术彻底改变执行模式。

1.2 应用生态发展的需求

Android应用数量和复杂度快速增长,Dalvik的JIT编译在长时间运行后会出现性能下降,尤其在游戏、视频编辑等高性能需求场景下表现不佳。ART的设计目标是通过预编译优化代码执行效率,同时降低运行时CPU负载,为开发者提供更接近原生应用的性能体验。

1.3 技术演进的必然性

Java虚拟机技术在桌面端已成熟,但移动端需要更适配的执行环境。Dalvik作为过渡方案解决了初期兼容性问题,而ART的出现标志着Android运行时向专业化、定制化方向发展,体现了对移动场景的深度优化。

二、字节码执行核心策略差异

2.1 Dalvik的JIT即时编译

Dalvik的JIT编译器位于dalvik/vm/Jit.cpp,核心逻辑通过JitCompileMethod函数实现:

// dalvik/vm/Jit.cpp
bool JitCompileMethod(const Method* method) {
    // 检查方法是否符合编译条件(如调用次数阈值)
    if (!shouldCompile(method)) {
        return false;
    }
    // 生成中间表示(IR)
    IRGenerator irGen(method);
    irGen.generateIR();
    // 优化IR(简单常量折叠等)
    optimizeIR(irGen.getIR());
    // 生成机器码
    MachineCode* machineCode = generateMachineCode(irGen.getIR());
    // 替换原字节码执行路径
    replaceBytecodeWithMachineCode(method, machineCode);
    return true;
}

JIT采用"解释-编译"混合模式:首次执行时解释执行,当方法调用次数超过阈值后触发编译。这种策略在启动速度和内存占用间取得平衡,但存在编译延迟和优化不足问题。

2.2 ART的AOT预编译机制

ART的AOT编译由dex2oat工具完成,核心代码位于art/tools/dex2oat/dex2oat.cc

// art/tools/dex2oat/dex2oat.cc
int main(int argc, char** argv) {
    // 解析命令行参数,获取输入DEX文件和目标架构
    CommandLineOptions options;
    if (!options.Parse(argc, argv)) {
        return -1;
    }
    // 加载DEX文件
    DexFile dexFile(options.input_dex_file);
    // 生成中间表示(HGraph)
    HGraphBuilder hGraphBuilder(&dexFile);
    HGraph* hGraph = hGraphBuilder.Build();
    // 多阶段优化(全局常量传播、死代码消除等)
    OptimizeHGraph(hGraph);
    // 针对目标架构生成机器码
    MachineCodeGenerator codeGen(hGraph, options.target_arch);
    codeGen.GenerateCode();
    // 输出包含机器码的OAT文件
    WriteOatFile(options.output_oat_file, codeGen.GetMachineCode());
    return 0;
}

AOT在应用安装阶段将DEX字节码编译为机器码,存储在.oat文件中。执行时直接加载机器码,避免了运行时编译开销,显著提升启动速度和执行效率。

2.3 混合编译的进化(ART 7.0+)

从Android 7.0开始,ART引入JIT+AOT混合模式:

  • 安装时进行部分AOT编译(快速路径)
  • 运行时通过JIT优化热点代码
  • 使用Profile-guided compilation记录运行时信息,在设备空闲时进行全量AOT编译 这种策略结合了AOT的快速启动和JIT的动态优化能力。

三、内存管理架构差异

3.1 Dalvik的堆内存模型

Dalvik采用线性堆内存布局,核心实现位于dalvik/vm/alloc/HeapSource.cpp

// dalvik/vm/alloc/HeapSource.cpp
HeapSource* HeapSource::CreateHeapSource() {
    // 初始化堆内存大小(默认16MB)
    size_t initialSize = kDefaultHeapSize;
    // 通过mmap系统调用分配连续内存
    void* heapMemory = mmap(nullptr, initialSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (heapMemory == MAP_FAILED) {
        // 内存分配失败处理
        return nullptr;
    }
    // 创建堆内存管理对象
    return new HeapSource(heapMemory, initialSize);
}

采用标记-清除垃圾回收算法,在dalvik/vm/gc/MarkSweep.cpp实现:

// dalvik/vm/gc/MarkSweep.cpp
void MarkSweepGC::CollectGarbage() {
    // 从根集合(栈、静态变量等)开始标记存活对象
    MarkRoots();
    // 扫描堆内存,清除未标记对象
    SweepHeap();
    // 压缩内存(可选)
    CompactHeapIfNeeded();
}

这种设计在小内存设备上表现良好,但存在内存碎片问题。

3.2 ART的分代式内存管理

ART采用分代式堆内存设计,分为:

  • 年轻代(Young Generation)
  • 老年代(Old Generation)
  • 大对象空间(Large Object Space) 堆初始化代码位于art/runtime/gc/heap.cc
// art/runtime/gc/heap.cc
void Heap::Initialize() {
    // 初始化年轻代(默认占堆的1/8)
    youngGen_.Initialize(this, kYoungGenRatio * totalHeapSize_);
    // 初始化老年代
    oldGen_.Initialize(this, totalHeapSize_ - youngGen_.GetCapacity());
    // 初始化大对象空间
    largeObjectSpace_.Initialize(this);
}

垃圾回收采用分代策略:

  • 年轻代使用复制算法(art/runtime/gc/collector/scavenge.cc
  • 老年代使用标记-压缩算法(art/runtime/gc/collector/marksweep.cc) 分代设计有效减少了垃圾回收暂停时间,提升了应用响应性。

四、类加载机制差异

4.1 Dalvik的类加载实现

Dalvik的类加载器在dalvik/vm/ClassPath.cpp实现:

// dalvik/vm/ClassPath.cpp
ClassObject* ClassPath::FindClass(const char* descriptor) {
    // 遍历类路径(DEX文件列表)
    for (const DexFile* dexFile : dexFiles_) {
        // 在DEX文件中查找类
        ClassObject* clazz = dexFile->FindClass(descriptor);
        if (clazz != nullptr) {
            return clazz;
        }
    }
    return nullptr;
}

采用传统的双亲委托模型,但对DEX文件的处理较为简单,缺乏深度验证机制。

4.2 ART的类加载增强

ART的类加载器体系在art/runtime/class_linker.cc实现:

// art/runtime/class_linker.cc
std::unique_ptr<Class> ClassLinker::LoadClass(ClassLoader* classLoader, const DexFile& dexFile, uint32_t classDefIndex) {
    // 验证类定义合法性
    if (!dexFile.IsValidClassDef(classDefIndex)) {
        return nullptr;
    }
    // 解析类引用
    ResolveClassReferences(dexFile, classDefIndex);
    // 链接类(准备阶段)
    LinkClass(classLoader, dexFile, classDefIndex);
    // 初始化类(如果需要)
    InitializeClassIfNeeded(classLoader, dexFile, classDefIndex);
    return std::unique_ptr<Class>(CreateClassObject(classLoader, dexFile, classDefIndex));
}

增加了以下特性:

  • 严格的类验证机制
  • 支持多DEX文件加载(MultiDex)
  • 更高效的类引用解析

五、JNI实现差异

5.1 Dalvik的JNI接口

Dalvik的JNI实现位于dalvik/vm/Jni.c

// dalvik/vm/Jni.c
JNIEXPORT jobject JNICALL dvmNewObject(JNIEnv* env, jclass clazz, jmethodID methodID, ...) {
    // 检查类是否已初始化
    if (!dvmIsClassInitialized(clazz)) {
        dvmInitClass(clazz);
    }
    // 分配对象内存
    Object* obj = dvmAllocObject(clazz, ALLOC_DONT_GC);
    if (obj == nullptr) {
        // 内存分配失败处理
        return nullptr;
    }
    // 调用构造函数
    va_list args;
    va_start(args, methodID);
    dvmCallMethodV((Method*)methodID, obj, args);
    va_end(args);
    return (jobject)obj;
}

JNI调用直接操作对象内存,缺乏严格的类型检查和安全防护。

5.2 ART的JNI增强

ART的JNI实现位于art/runtime/jni/java_vm_ext.cc

// art/runtime/jni/java_vm_ext.cc
jobject JavaVMExt::NewObject(JNIEnv* env, jclass clazz, jmethodID methodID, ...) {
    // 验证类和方法的有效性
    if (!IsClassInitialized(clazz) || !IsValidMethodID(methodID)) {
        ThrowException(env, "Invalid class or method");
        return nullptr;
    }
    // 通过内存分配器创建对象
    ObjPtr<mirror::Object> obj = runtime_->GetHeap()->AllocObject<mirror::Object>(clazz);
    if (obj == nullptr) {
        // 内存不足处理
        ThrowOutOfMemoryError(env);
        return nullptr;
    }
    // 调用构造函数
    va_list args;
    va_start(args, methodID);
    InvokeDirectMethod(runtime_, env, methodID, obj, args);
    va_end(args);
    return env->ToLocal(obj.Ptr());
}

增加了:

  • 严格的参数验证
  • 异常处理机制
  • 内存安全检查
  • 线程安全优化

六、调试与性能分析支持

6.1 Dalvik的调试能力

Dalvik通过jdwp实现调试支持,代码位于dalvik/vm/Jdwp.cpp

// dalvik/vm/Jdwp.cpp
void JdwpThread::HandleCommand() {
    // 接收调试命令
    JdwpPacket packet;
    if (!ReceivePacket(&packet)) {
        // 接收失败处理
        return;
    }
    // 解析命令类型
    switch (packet.GetCommandSet()) {
        case JDWP_COMMAND_SET_THREAD:
            HandleThreadCommand(packet);
            break;
        case JDWP_COMMAND_SET_CLASS:
            HandleClassCommand(packet);
            break;
        // 其他命令处理
    }
    // 发送响应
    SendResponsePacket(packet);
}

支持基本的断点调试和线程管理,但缺乏深度性能分析能力。

6.2 ART的调试增强

ART在调试支持上进行了全面升级:

  • 集成Systrace进行系统级性能分析
  • 支持Perfetto进行详细的运行时追踪
  • 增强的JDWP实现(art/runtime/jdwp/jdwp_thread.cc
// art/runtime/jdwp/jdwp_thread.cc
void JdwpThread::HandleCheckCapabilities(JdwpPacket* packet) {
    // 检查调试器权限
    if (!HasDebugPermission(packet->GetClientID())) {
        SendErrorResponse(packet, JDWP_ERROR_INVALID_ACCESS);
        return;
    }
    // 返回支持的功能列表
    SendCapabilitiesResponse(packet);
}

提供更丰富的调试接口和安全机制。

七、安全机制差异

7.1 Dalvik的安全设计

Dalvik主要依赖Linux内核的进程隔离机制,自身安全防护较弱。在权限管理方面,通过AndroidManifest.xml声明权限,但缺乏运行时检查。垃圾回收机制未对内存安全做特殊处理,存在缓冲区溢出风险。

7.2 ART的安全增强

ART在安全方面进行了大量改进:

  • SELinux集成:在art/runtime/selinux/selinux.cc实现安全上下文管理
// art/runtime/selinux/selinux.cc
bool Selinux::CheckAccess(const std::string& type, const std::string& target, const std::string& perm) {
    // 调用SELinux内核接口进行权限检查
    return selinux_check_access(type.c_str(), target.c_str(), perm.c_str()) == 0;
}
  • 内存安全:增加指针有效性检查和缓冲区溢出防护
  • 代码签名验证:在应用安装阶段严格验证签名