码字不易,请点点关注~~
一、Android Runtime概述
Android Runtime(ART)是Android系统自5.0版本后采用的应用运行环境,替代了之前的Dalvik虚拟机。ART采用AOT(Ahead - Of - Time)编译技术,在应用安装时将字节码编译成机器码,极大提升了应用的执行效率 。在整个运行环境中,异常处理和跳转指令是保障程序正常执行和流程控制的关键机制。
ART的核心代码分布在多个模块中,主要位于art/runtime
目录下,涉及线程管理、内存分配、垃圾回收、指令执行等多个子系统。异常处理和跳转指令的实现依赖于这些子系统的协同工作。例如,异常发生时需要暂停当前线程、保存现场、创建异常对象,这些操作都与线程管理和内存分配紧密相关;而跳转指令的执行则涉及到指令流的控制、程序计数器(PC)的更新等,与指令执行子系统密切相连。
从整体架构上看,ART通过一系列的类和函数来实现异常处理和跳转指令。异常处理涉及到ExceptionHandler
类、ShadowFrame
类等,而跳转指令的实现则依赖于Interpreter
(解释器)、JitCompiler
(即时编译器)和AotCompiler
(提前编译器)等模块中的相关代码。接下来,我们将深入到源码层面,详细分析这些机制的实现原理。
二、异常处理基础结构
2.1 异常对象的创建与表示
在ART中,异常对象基于Java的异常体系构建,对应mirror::Throwable
类及其子类。当异常发生时,首先需要创建相应的异常对象。在art/runtime/exception_handler.cc
文件中,我们可以找到创建异常对象的关键代码:
cpp
// 创建栈溢出异常对象
mirror::Throwable* ExceptionHandler::CreateStackOverflowException(ScopedObjectAccess& soa) {
// 获取StackOverflowError类的镜像
mirror::Class* stack_overflow_error_class = soa.Decode<mirror::Class*>(
Runtime::Current()->GetJavaLangClass(kJavaLangStackOverflowError));
DCHECK(stack_overflow_error_class != nullptr);
// 分配异常对象内存
mirror::Throwable* exception = soa.AllocObject<mirror::Throwable>(stack_overflow_error_class);
// 设置异常消息(这里可以根据具体情况设置)
const char* message = "Stack overflow occurred";
SetExceptionMessage(soa, exception, message);
return exception;
}
上述代码首先通过Runtime::Current()->GetJavaLangClass(kJavaLangStackOverflowError)
获取StackOverflowError
类的镜像,这一步依赖于ART对Java类加载机制的实现。然后使用soa.AllocObject
分配异常对象内存,该函数内部会调用堆内存分配函数,确保异常对象在堆上正确创建。最后通过SetExceptionMessage
设置异常消息,方便开发者定位问题。
对于其他类型的异常,创建过程类似,只是获取的类镜像不同。例如创建NullPointerException
异常:
cpp
mirror::Throwable* ExceptionHandler::CreateNullPointerException(ScopedObjectAccess& soa) {
mirror::Class* null_pointer_exception_class = soa.Decode<mirror::Class*>(
Runtime::Current()->GetJavaLangClass(kJavaLangNullPointerException));
DCHECK(null_pointer_exception_class != nullptr);
mirror::Throwable* exception = soa.AllocObject<mirror::Throwable>(null_pointer_exception_class);
const char* message = "Attempt to access a null object";
SetExceptionMessage(soa, exception, message);
return exception;
}
2.2 异常处理的上下文管理
异常发生时,需要管理当前的执行上下文,包括线程状态、局部变量表、操作数栈等信息。ShadowFrame
类在这一过程中起到了关键作用。ShadowFrame
用于模拟方法调用栈帧,保存了方法执行时的各种状态。
cpp
// ShadowFrame类的关键成员变量
class ShadowFrame FINAL {
public:
// 指向当前方法的指针
ArtMethod* method_;
// 方法执行的字节码PC值
uint32_t dex_pc_;
// 局部变量表
uint32_t* vregs_;
// 操作数栈指针
uint32_t* stack_pointer_;
// 其他成员函数...
};
当异常发生时,ExceptionHandler
会暂停当前线程,并获取当前的ShadowFrame
:
cpp
void ExceptionHandler::HandleException(Thread* thread, siginfo_t* info, void* context) {
// 暂停线程
thread->Suspend();
// 获取当前线程的ShadowFrame
ShadowFrame* shadow_frame = thread->TopShadowFrame();
// 保存局部变量表和操作数栈状态(简化示例,实际更复杂)
SaveFrameState(shadow_frame);
// 创建异常对象
mirror::Throwable* exception = CreateException(thread);
// 设置线程的异常对象
thread->SetException(soa, exception);
// 开始异常处理流程
thread->UnwindException();
}
SaveFrameState
函数负责保存ShadowFrame
中的局部变量表和操作数栈状态,以便在异常处理完成后能够恢复现场。而thread->UnwindException
则启动了异常传播和处理流程,该函数会沿着调用栈向上查找合适的异常处理代码块。
三、异常传播机制
3.1 调用栈遍历与异常查找
当异常发生时,ART需要遍历调用栈,查找能够处理该异常的代码块。这一过程从当前发生异常的方法开始,逐步向上层方法追溯。在art/runtime/thread.cc
中可以找到相关实现:
cpp
void Thread::UnwindException() {
ScopedObjectAccess soa(this);
mirror::Throwable* exception = GetException(soa);
DCHECK(exception != nullptr);
ShadowFrame* shadow_frame = TopShadowFrame();
while (shadow_frame != nullptr) {
ArtMethod* method = shadow_frame->method_;
// 检查当前方法是否有异常处理代码(简化判断)
if (method->HasExceptionHandler()) {
// 找到异常处理代码,进行处理
HandleExceptionInMethod(shadow_frame, exception);
return;
}
// 向上移动到上一个栈帧
shadow_frame = shadow_frame->GetLink();
}
// 如果没有找到合适的异常处理代码,终止进程或进行默认处理
HandleUnhandledException(exception);
}
上述代码中,HasExceptionHandler
函数用于判断当前方法是否包含异常处理代码块,其实现依赖于方法字节码中的异常表信息。如果找到异常处理代码,则调用HandleExceptionInMethod
进行处理;否则继续向上层栈帧查找。
3.2 异常处理代码的执行
当找到异常处理代码后,ART需要调整程序执行流程,跳转到异常处理代码块执行。这涉及到程序计数器(PC)的更新和局部变量的重新设置。
cpp
void Thread::HandleExceptionInMethod(ShadowFrame* shadow_frame, mirror::Throwable* exception) {
ArtMethod* method = shadow_frame->method_;
// 获取异常处理代码的起始PC值
uint32_t handler_pc = method->GetExceptionHandlerPc(exception->GetClass());
// 更新当前栈帧的PC值
shadow_frame->dex_pc_ = handler_pc;
// 将异常对象压入操作数栈(如果需要在异常处理代码中使用)
shadow_frame->PushObject(exception);
// 继续执行方法,此时会从异常处理代码开始执行
ExecuteMethod(shadow_frame);
}
GetExceptionHandlerPc
函数根据异常对象的类型,从方法的异常表中查找对应的异常处理代码起始PC值。更新dex_pc_
后,通过ExecuteMethod
继续执行方法,程序便会从异常处理代码块开始执行。在异常处理代码中,可以对异常进行处理,如打印日志、释放资源等操作。
四、跳转指令基础
4.1 跳转指令的分类与作用
在ART中,跳转指令主要用于控制程序的执行流程,包括条件跳转(如if - then
语句)、无条件跳转(如goto
语句)、方法调用和返回等。不同类型的跳转指令在字节码层面有不同的表示,并且在ART的实现中也有各自的处理方式。
- 条件跳转指令 :根据条件判断结果决定是否执行跳转,例如
if - eq
(如果相等则跳转)、if - ne
(如果不相等则跳转)等。这类指令通常会结合比较操作,根据操作数栈中的值进行条件判断。 - 无条件跳转指令 :直接改变程序执行流程,跳转到指定的目标地址,如
goto
指令。 - 方法调用指令:用于调用其他方法,包括静态方法调用、实例方法调用等。调用方法时需要处理参数传递、保存现场等操作。
- 方法返回指令:用于从方法中返回,将控制权交还给调用者,并处理返回值传递。
4.2 跳转指令与程序计数器
程序计数器(PC)在跳转指令执行过程中起着关键作用,它记录了当前程序执行的位置。当执行跳转指令时,PC的值会被更新为目标地址,从而改变程序的执行流程。在解释执行模式下,Interpreter
类负责更新PC值。
cpp
// 解释器中处理条件跳转指令的示例代码
static void ExecuteIfEq(Thread* self, ShadowFrame* shadow_frame, uint16_t*& current_pc) {
// 从操作数栈弹出两个值进行比较
uint32_t value2 = shadow_frame->PopInt();
uint32_t value1 = shadow_frame->PopInt();
// 判断条件是否成立
if (value1 == value2) {
// 计算跳转偏移量
int16_t offset = DecodeSignedLeb128(¤t_pc);
// 更新PC值,实现跳转
current_pc += offset;
} else {
// 条件不成立,继续执行下一条指令
current_pc++;
}
}
上述代码中,ExecuteIfEq
函数首先从操作数栈弹出两个值进行比较,然后根据比较结果决定是否更新PC值。如果条件成立,通过计算跳转偏移量并更新PC值,使程序跳转到目标地址;否则继续执行下一条指令。在JIT编译和AOT编译模式下,跳转指令的处理会通过生成相应的机器码来实现PC值的更新,但基本原理与解释执行类似。
五、条件跳转指令实现
5.1 字节码解析与条件判断
条件跳转指令在字节码中以特定的操作码表示,ART的解释器和编译器需要解析这些操作码,并根据操作数栈中的值进行条件判断。以if - lt
(如果小于则跳转)指令为例,在解释器中的处理如下:
cpp
// 解释器中处理if - lt指令的代码
static void ExecuteIfLt(Thread* self, ShadowFrame* shadow_frame, uint16_t*& current_pc) {
// 从操作数栈弹出两个值
uint32_t value2 = shadow_frame->PopInt();
uint32_t value1 = shadow_frame->PopInt();
// 进行小于比较
if (value1 < value2) {
// 计算跳转偏移量
int16_t offset = DecodeSignedLeb128(¤t_pc);
// 更新PC值,实现跳转
current_pc += offset;
} else {
// 条件不成立,继续执行下一条指令
current_pc++;
}
}
在上述代码中,首先从操作数栈弹出两个值,然后进行小于比较操作。如果比较结果为真,则通过DecodeSignedLeb128
函数解析跳转偏移量,并更新current_pc
实现跳转;否则current_pc
自增,继续执行下一条指令。在JIT编译过程中,条件跳转指令会被编译成相应的机器码条件分支指令,如JL
(小于则跳转)指令,其原理也是基于条件判断和目标地址计算。
5.2 编译优化对条件跳转的影响
在JIT编译和AOT编译过程中,会对条件跳转指令进行优化,以提高执行效率。一种常见的优化方式是分支预测。现代处理器通常具备分支预测功能,编译器可以通过特定的指令或代码结构,引导处理器更准确地预测条件跳转的结果,减少流水线停顿。
cpp
// JIT编译器中对条件跳转进行分支预测优化的示例
void JitCompiler::OptimizeConditionalJump(HInstruction* jump_instruction) {
// 判断跳转指令的类型
if (jump_instruction->IsConditionalJump()) {
// 获取条件判断的操作数
HInstruction* condition = jump_instruction->GetCondition();
// 根据条件的特点,设置分支预测提示(假设存在相关函数)
SetBranchPredictionHint(condition, kLikelyTrue);
}
}
上述代码中,OptimizeConditionalJump
函数首先判断指令是否为条件跳转指令,然后获取条件判断的操作数。通过SetBranchPredictionHint
函数设置分支预测提示,告诉处理器该条件跳转大概率会发生(kLikelyTrue
)或不会发生(kLikelyFalse
),从而帮助处理器更高效地执行程序。此外,编译器还可能通过代码重排序等优化手段,进一步提高条件跳转指令的执行效率。
六、无条件跳转指令实现
6.1 直接跳转的实现原理
无条件跳转指令,如goto
指令,在ART中的实现相对直接,主要是通过更新程序计数器(PC)的值,将程序执行流程跳转到指定的目标地址。在解释器中,处理goto
指令的代码如下:
cpp
// 解释器中处理goto指令的代码
static void ExecuteGoto(Thread* self, ShadowFrame* shadow_frame, uint16_t*& current_pc) {
// 计算跳转偏移量
int16_t offset = DecodeSignedLeb128(¤t_pc);
// 更新PC值,实现跳转
current_pc += offset;
}
上述代码中,ExecuteGoto
函数通过DecodeSignedLeb128
解析goto
指令中的跳转偏移量,然后直接将该偏移量加到current_pc
上,实现程序执行流程的跳转。在JIT编译和AOT编译过程中,无条件跳转指令会被编译成机器码中的无条件跳转指令,如JMP
指令,其作用同样是直接改变指令执行地址。
6.2 跳转目标地址的计算与解析
对于无条件跳转指令,跳转目标地址的计算和解析是关键。在字节码层面,跳转目标地址通常以相对于当前PC的偏移量表示。在解释执行时,需要根据这个偏移量计算出实际的目标PC值;在编译过程中,则需要将偏移量转换为具体的机器码地址。
cpp
// 计算跳转目标PC值的通用函数
uint32_t CalculateJumpTargetPc(uint16_t* current_pc, int16_t offset) {
// 获取当前PC值
uint32_t current_pc_value = reinterpret_cast<uint32_t>(current_pc - GetInstructionStart());
// 计算目标PC值
return current_pc_value + offset;
}
上述代码中,CalculateJumpTargetPc
函数首先获取当前PC值(通过计算相对于指令起始地址的偏移),然后加上跳转偏移量,得到目标PC值。在JIT编译和AOT编译过程中,会在生成机器码时直接将目标地址计算好并嵌入到相应的跳转指令中,确保程序能够准确跳转到目标位置。
七、方法调用指令实现
7.1 方法调用的参数传递
方法调用指令在ART中涉及到参数传递、栈帧创建和方法执行等多个步骤。首先是参数传递,ART根据方法的参数列表,将调用者提供的参数按照一定的规则传递给被调用方法。在解释器中,参数传递的实现如下:
cpp
// 解释器中处理方法调用参数传递的代码
static void ExecuteInvokeVirtual(Thread* self, ShadowFrame* shadow_frame, uint16_t*& current_pc) {
// 获取被调用方法的索引
uint16_t method_index = DecodeUnsignedLeb128(¤t_pc);
// 获取被调用方法的镜像
ArtMethod* method = GetMethodFromIndex(method_index);
// 获取参数个数
uint32_t parameter_count = method->GetParameterSize();
// 从操作数栈弹出参数,并按照顺序传递给被调用方法
std::vector<uint32_t> parameters;
for (uint32_t i = 0; i < parameter_count; ++i) {
parameters.push_back(shadow_frame->PopInt());
}
// 创建新的栈帧,并将参数复制到新栈帧的局部变量表中
ShadowFrame* new_frame = CreateShadowFrame(method, current_pc, shadow_frame->GetStackPointer());
for (uint32_t i = 0; i < parameter_count; ++i) {
new_frame->SetVReg(i, parameters[parameter_count - 1 - i]);
}
// 将新栈帧压入调用栈
self->PushShadowFrame(new_frame);
// 开始执行被调用方法
ExecuteMethod(new_frame);
}
上述代码中,ExecuteInvokeVirtual
函数首先获取被调用方法的索引和镜像,然后根据方法的参数个数从操作
上述代码中,ExecuteInvokeVirtual
函数首先获取被调用方法的索引和镜像,然后根据方法的参数个数从操作数栈弹出参数。由于操作数栈是后进先出(LIFO)的结构,而参数传递需要按照从左到右的顺序,因此在将参数复制到新栈帧的局部变量表时,需要进行逆序处理。创建新的ShadowFrame
后,将参数依次设置到局部变量表的相应位置,然后将新栈帧压入线程的调用栈,开始执行被调用方法。
在JIT编译和AOT编译模式下,参数传递会通过生成特定的机器码来实现。对于ARM架构,参数通常通过寄存器(如R0 - R3)和栈来传递。编译器会根据方法的参数类型和数量,生成相应的代码将参数放入正确的寄存器或栈位置。例如:
cpp
// JIT编译器中生成参数传递代码的示例
void JitCompiler::GenerateParameterPassing(ArtMethod* method) {
// 获取方法的参数信息
uint32_t parameter_count = method->GetParameterSize();
bool is_static = method->IsStatic();
// 计算需要通过寄存器传递的参数数量
uint32_t registers_used = std::min(parameter_count, kMaxParameterRegisters);
// 生成将参数从当前栈帧复制到寄存器的代码
for (uint32_t i = 0; i < registers_used; ++i) {
// 获取参数在局部变量表中的位置
uint32_t local_index = is_static ? i : i + 1; // 非静态方法需要跳过this指针
// 生成将局部变量复制到寄存器的代码
GenerateMoveLocalToRegister(local_index, GetRegisterForParameter(i));
}
// 生成将剩余参数压入栈的代码
for (uint32_t i = registers_used; i < parameter_count; ++i) {
uint32_t local_index = is_static ? i : i + 1;
GeneratePushLocalToStack(local_index);
}
}
上述代码展示了JIT编译器如何生成参数传递的机器码。首先确定通过寄存器传递的参数数量(通常ARM架构最多使用4个寄存器传递参数),然后生成代码将这些参数从局部变量表复制到对应的寄存器中。对于超出寄存器数量的参数,则生成代码将其压入调用栈。
7.2 方法查找与解析
方法调用指令执行时,需要确定具体要调用的方法实现。对于虚方法调用(如invoke - virtual
),这涉及到动态方法查找;而对于静态方法调用(如invoke - static
),则可以在编译时确定具体方法。在ART中,方法查找的实现如下:
cpp
// 动态方法查找的实现
ArtMethod* ArtMethod::ResolveVirtualMethod(ObjPtr<mirror::Object> receiver, uint32_t method_idx) {
// 获取接收者对象的类
mirror::Class* receiver_class = receiver->GetClass();
// 从类的虚方法表中查找方法
ArtMethod* method = receiver_class->FindVirtualMethod(method_idx);
if (method != nullptr) {
return method;
}
// 如果在当前类中未找到,则在父类中继续查找
mirror::Class* super_class = receiver_class->GetSuperClass();
while (super_class != nullptr) {
method = super_class->FindVirtualMethod(method_idx);
if (method != nullptr) {
return method;
}
super_class = super_class->GetSuperClass();
}
// 如果仍未找到,抛出异常
ThrowNoSuchMethodError(receiver_class, method_idx);
return nullptr;
}
上述代码中,ResolveVirtualMethod
函数首先获取接收者对象的类,然后在该类的虚方法表中查找指定索引的方法。如果找不到,则递归地在父类中查找,直到找到匹配的方法或到达类层次结构的顶部。如果最终仍未找到方法,则抛出NoSuchMethodError
异常。
对于接口方法调用(如invoke - interface
),方法查找的实现略有不同,因为接口方法可以被多个类实现,需要遍历实现该接口的所有类。ART通过维护接口方法表(Interface Method Table,IMT)来优化接口方法调用的性能:
cpp
// 接口方法查找的实现
ArtMethod* ArtMethod::ResolveInterfaceMethod(ObjPtr<mirror::Object> receiver, uint32_t method_idx) {
// 获取接收者对象的类
mirror::Class* receiver_class = receiver->GetClass();
// 从类的接口方法表中查找方法
ArtMethod* method = receiver_class->FindInterfaceMethod(method_idx);
if (method != nullptr) {
return method;
}
// 如果在接口方法表中未找到,则进行慢速查找
// 遍历实现的所有接口,查找匹配的方法
// 此处省略具体实现代码
return SlowResolveInterfaceMethod(receiver_class, method_idx);
}
接口方法表是一个映射表,将接口方法的索引映射到具体的实现方法。在类加载时,ART会为实现了接口的类构建接口方法表,从而加速接口方法的调用。
7.3 方法调用栈帧的创建与销毁
方法调用时,需要创建新的栈帧来保存方法执行的上下文信息;方法返回时,需要销毁该栈帧并恢复调用者的上下文。在ART中,栈帧的创建和销毁由ShadowFrame
类负责:
cpp
// 创建新的栈帧
ShadowFrame* ShadowFrame::Create(ArtMethod* method, uint32_t dex_pc, uintptr_t sp) {
// 计算栈帧所需的内存大小
size_t size = SizeOf(method);
// 分配内存
uint8_t* memory = reinterpret_cast<uint8_t*>(malloc(size));
// 初始化栈帧对象
ShadowFrame* result = new (memory) ShadowFrame(method, dex_pc, sp);
// 初始化局部变量表
result->InitializeLocals();
return result;
}
// 销毁栈帧
void Thread::PopShadowFrame() {
ShadowFrame* current_frame = TopShadowFrame();
if (current_frame != nullptr) {
// 获取调用者的栈帧
ShadowFrame* caller_frame = current_frame->GetLink();
// 恢复调用者的PC值
uint32_t caller_dex_pc = current_frame->GetCallerDexPc();
if (caller_frame != nullptr) {
caller_frame->SetDexPc(caller_dex_pc);
}
// 恢复调用者的栈指针
SetStackPointer(current_frame->GetCallerSp());
// 释放当前栈帧的内存
free(current_frame);
// 更新线程的栈帧指针
SetTopShadowFrame(caller_frame);
}
}
在创建栈帧时,Create
函数首先计算栈帧所需的内存大小,包括局部变量表、操作数栈等空间。然后分配内存并初始化栈帧对象,设置方法指针、PC值等信息。在销毁栈帧时,PopShadowFrame
函数从线程的调用栈中弹出当前栈帧,恢复调用者的PC值和栈指针,然后释放当前栈帧占用的内存。
在方法调用过程中,还需要处理返回值的传递。当方法执行完毕返回时,返回值会被压入调用者的操作数栈中:
cpp
// 处理方法返回的代码
static void ExecuteReturnVoid(Thread* self, ShadowFrame* shadow_frame) {
// 获取调用者的栈帧
ShadowFrame* caller_frame = shadow_frame->GetLink();
// 销毁当前栈帧
self->PopShadowFrame();
// 恢复执行调用者的方法
if (caller_frame != nullptr) {
ExecuteMethod(caller_frame);
}
}
static void ExecuteReturnInt(Thread* self, ShadowFrame* shadow_frame) {
// 从当前栈帧的操作数栈获取返回值
uint32_t return_value = shadow_frame->PopInt();
// 获取调用者的栈帧
ShadowFrame* caller_frame = shadow_frame->GetLink();
// 销毁当前栈帧
self->PopShadowFrame();
// 将返回值压入调用者的操作数栈
if (caller_frame != nullptr) {
caller_frame->PushInt(return_value);
// 恢复执行调用者的方法
ExecuteMethod(caller_frame);
}
}
上述代码展示了无返回值(return - void
)和整数返回值(return - int
)的处理过程。对于有返回值的方法,在返回时从当前栈帧的操作数栈中获取返回值,然后在销毁当前栈帧后,将返回值压入调用者的操作数栈,以便调用者继续执行后续操作。
八、方法返回指令实现
8.1 返回值的处理
方法返回指令负责将方法的执行结果返回给调用者,并恢复调用者的执行上下文。在ART中,不同类型的返回指令(如return - void
、return - int
、return - object
等)处理方式略有不同,但基本原理一致。以return - object
指令为例:
cpp
// 处理对象返回的代码
static void ExecuteReturnObject(Thread* self, ShadowFrame* shadow_frame) {
// 从当前栈帧的操作数栈获取返回的对象引用
mirror::Object* return_value = shadow_frame->PopObject();
// 获取调用者的栈帧
ShadowFrame* caller_frame = shadow_frame->GetLink();
// 销毁当前栈帧
self->PopShadowFrame();
// 将返回值压入调用者的操作数栈
if (caller_frame != nullptr) {
caller_frame->PushObject(return_value);
// 恢复执行调用者的方法
ExecuteMethod(caller_frame);
}
}
上述代码中,ExecuteReturnObject
函数首先从当前栈帧的操作数栈中弹出返回的对象引用,然后获取调用者的栈帧并销毁当前栈帧。接着将返回的对象引用压入调用者的操作数栈,并恢复调用者的执行。对于其他类型的返回指令,处理过程类似,只是获取和压入返回值的类型不同。
在处理返回值时,还需要考虑返回值的类型检查。例如,当返回一个对象时,需要确保该对象的类型与方法声明的返回类型兼容:
cpp
// 检查返回值类型的代码
bool CheckReturnType(mirror::Object* return_value, mirror::Class* return_type) {
if (return_value == nullptr) {
// null可以赋值给任何引用类型
return true;
}
// 获取返回对象的实际类型
mirror::Class* actual_type = return_value->GetClass();
// 检查实际类型是否是返回类型的子类或实现了返回类型接口
return actual_type->IsAssignableFrom(return_type);
}
上述代码中,CheckReturnType
函数首先处理返回值为null
的情况,因为null
可以赋值给任何引用类型。然后获取返回对象的实际类型,并检查该类型是否与方法声明的返回类型兼容。如果不兼容,可能会抛出ClassCastException
异常。
8.2 栈帧恢复与执行流程跳转
方法返回时,除了处理返回值,还需要恢复调用者的执行上下文,包括PC值、局部变量表和操作数栈等。在ART中,这些操作由PopShadowFrame
函数和相关代码完成:
cpp
// 恢复调用者上下文的代码
void Thread::PopShadowFrame() {
ShadowFrame* current_frame = TopShadowFrame();
if (current_frame != nullptr) {
// 获取调用者的栈帧
ShadowFrame* caller_frame = current_frame->GetLink();
// 恢复调用者的PC值
uint32_t caller_dex_pc = current_frame->GetCallerDexPc();
if (caller_frame != nullptr) {
caller_frame->SetDexPc(caller_dex_pc);
}
// 恢复调用者的栈指针
SetStackPointer(current_frame->GetCallerSp());
// 释放当前栈帧的内存
free(current_frame);
// 更新线程的栈帧指针
SetTopShadowFrame(caller_frame);
}
}
在方法返回时,调用PopShadowFrame
函数会从线程的调用栈中弹出当前栈帧,恢复调用者的PC值和栈指针。然后释放当前栈帧占用的内存,并更新线程的栈帧指针,指向调用者的栈帧。之后,通过ExecuteMethod
函数继续执行调用者的方法,程序流程便从被调用方法返回到了调用者。
对于尾调用(Tail Call)优化,ART也有相应的实现。尾调用是指一个函数的最后一个操作是调用另一个函数,此时可以复用当前函数的栈帧,避免创建新的栈帧,从而节省内存并提高性能。在ART中,尾调用优化的实现如下:
cpp
// 尾调用优化的代码
bool TryTailCallOptimization(Thread* self, ShadowFrame* shadow_frame, ArtMethod* target_method) {
// 检查是否满足尾调用优化的条件
if (!IsTailCallOptimizationSupported(target_method)) {
return false;
}
// 获取当前方法的参数
std::vector<uint32_t> parameters = CollectParameters(shadow_frame);
// 重置当前栈帧为目标方法的栈帧
shadow_frame->ResetForMethod(target_method);
// 设置目标方法的参数
SetParameters(shadow_frame, parameters);
// 更新PC值,开始执行目标方法
shadow_frame->SetDexPc(0);
return true;
}
上述代码中,TryTailCallOptimization
函数首先检查是否满足尾调用优化的条件,如目标方法是否为非同步方法、是否有适当的返回类型等。如果满足条件,则收集当前方法的参数,重置当前栈帧为目标方法的栈帧,并设置目标方法的参数。然后更新PC值,开始执行目标方法,从而避免了创建新的栈帧。
九、异常处理与跳转指令的交互
9.1 异常处理中的跳转机制
当异常发生时,ART需要跳转到合适的异常处理代码块执行。这一过程涉及到特殊的跳转机制,与普通的跳转指令有所不同。在异常处理中,跳转目标地址由异常表(Exception Table)确定,而不是由指令中的偏移量直接决定。
异常表是方法字节码中的一个数据结构,记录了每个异常处理代码块的起始位置、结束位置和处理代码的位置。当异常发生时,ART会遍历异常表,找到最匹配的异常处理代码块:
cpp
// 查找异常处理代码的实现
uint32_t ArtMethod::FindExceptionHandlerPc(uint32_t dex_pc, mirror::Class* exception_class) {
// 获取方法的异常表
const DexFile::CodeItem* code_item = GetCodeItem();
if (code_item == nullptr || code_item->tries_size_ == 0) {
return kInvalidDexPc; // 没有异常处理代码
}
// 获取异常表
const DexFile::TryItem* tries = GetTryItems(code_item);
const uint8_t* handlers = GetExceptionHandlers(code_item);
// 遍历异常表,查找匹配的处理代码
for (uint32_t i = 0; i < code_item->tries_size_; ++i) {
const DexFile::TryItem& try_item = tries[i];
// 检查异常发生的PC值是否在try块范围内
if (dex_pc >= try_item.start_addr_ && dex_pc < try_item.start_addr_ + try_item.insn_count_) {
// 找到匹配的try块,查找对应的catch块
uint32_t handler_pc = FindMatchingHandler(handlers, try_item, exception_class);
if (handler_pc != kInvalidDexPc) {
return handler_pc;
}
}
}
return kInvalidDexPc; // 没有找到匹配的处理代码
}
上述代码中,FindExceptionHandlerPc
函数首先获取方法的异常表,然后遍历异常表中的每个try
块,检查异常发生的PC值是否在try
块范围内。如果找到匹配的try
块,则调用FindMatchingHandler
函数查找对应的catch
块,即能够处理当前异常类型的代码块。
找到异常处理代码的PC值后,ART需要执行一个特殊的跳转,将程序流程转移到异常处理代码块。这个跳转与普通跳转不同,因为它需要保存更多的上下文信息,以便在异常处理完成后能够正确恢复:
cpp
// 跳转到异常处理代码的实现
void Thread::JumpToExceptionHandler(uint32_t handler_pc, mirror::Throwable* exception) {
ShadowFrame* current_frame = TopShadowFrame();
if (current_frame != nullptr) {
// 更新当前栈帧的PC值,指向异常处理代码
current_frame->SetDexPc(handler_pc);
// 将异常对象压入操作数栈,以便在异常处理代码中可以使用
current_frame->PushObject(exception);
// 标记当前线程正在处理异常
SetHandlingException(true);
// 继续执行,此时会从异常处理代码开始执行
ExecuteMethod(current_frame);
}
}
上述代码中,JumpToExceptionHandler
函数更新当前栈帧的PC值,指向异常处理代码的起始位置。然后将异常对象压入操作数栈,以便在异常处理代码中可以通过catch
语句捕获该异常。最后标记线程正在处理异常,并继续执行方法,程序流程便转移到了异常处理代码块。
9.2 跳转指令对异常传播的影响
普通的跳转指令(如条件跳转、无条件跳转)在执行时,可能会影响异常的传播路径。例如,当一个跳转指令跳过了某个try
块的代码时,如果在被跳过的代码中发生异常,该异常将无法被该try
块对应的catch
块捕获。
在ART中,这种情况通过异常表的设计来处理。异常表中的try
块范围是静态确定的,与程序的实际执行路径无关。因此,即使某个try
块的代码被跳转指令跳过,异常表中仍然会记录该try
块的信息。当异常发生时,ART会根据异常发生的PC值和异常表来确定处理代码,而不是根据程序的执行路径。
例如,考虑以下Java代码:
java
try {
if (condition) {
// 代码块A
} else {
// 代码块B
}
} catch (Exception e) {
// 异常处理代码
}
对应的字节码可能包含一个try
块,覆盖了if - else
语句的整个范围。如果条件跳转指令导致程序跳过了代码块A或代码块B,当异常发生时,ART仍然会检查该try
块对应的异常处理代码是否能够处理该异常。
然而,某些特殊的跳转指令,如goto
指令,如果跳转到了方法的外部(例如通过抛出异常或执行return
语句),可能会影响异常的传播。在这种情况下,ART会按照正常的异常传播机制,向上层调用栈查找合适的异常处理代码。
十、异常处理与跳转指令的性能优化
10.1 异常处理的性能优化策略
异常处理机制在现代编程语言中是必不可少的,但它的性能开销一直是关注的焦点。ART采用了多种优化策略来减少异常处理的性能开销。
其中一种优化策略是异常表的快速查找。在早期的实现中,ART遍历异常表的方式比较简单,即线性遍历每个try
块。随着方法复杂度的增加,这种方式可能会变得低效。现代ART实现中,对异常表进行了优化,采用了二分查找等算法来加速查找过程:
cpp
// 优化后的异常处理查找代码
uint32_t ArtMethod::FindExceptionHandlerPcOptimized(uint32_t dex_pc, mirror::Class* exception_class) {
// 获取方法的异常表
const DexFile::CodeItem* code_item = GetCodeItem();
if (code_item == nullptr || code_item->tries_size_ == 0) {
return kInvalidDexPc;
}
// 获取异常表
const DexFile::TryItem* tries = GetTryItems(code_item);
const uint8_t* handlers = GetExceptionHandlers(code_item);
// 使用二分查找快速定位可能包含dex_pc的try块
uint32_t left = 0;
uint32_t right = code_item->tries_size_ - 1;
while (left <= right) {
uint32_t mid = left + (right - left) / 2;
const DexFile::TryItem& try_item = tries[mid];
if (dex_pc < try_item.start_addr_) {
right = mid - 1;
} else if (dex_pc >= try_item.start_addr_ + try_item.insn_count_) {
left = mid + 1;
} else {
// 找到匹配的try块,查找对应的catch块
uint32_t handler_pc = FindMatchingHandler(handlers, try_item, exception_class);
if (handler_pc != kInvalidDexPc) {
return handler_pc;
}
break;
}
}
return kInvalidDexPc;
}
上述代码中,FindExceptionHandlerPcOptimized
函数使用二分查找算法来快速定位可能包含异常发生PC值的try
块。通过比较PC值与try
块的起始和结束位置,不断缩小查找范围,从而提高查找效率。这种优化在处理包含大量try
块的复杂方法时尤为有效。
另一种优化策略是异常表的预编译。在AOT编译过程中,ART可以将异常表信息编译到机器码中,使异常处理代码能够直接访问优化后的异常表结构,进一步减少查找时间。
10.2 跳转指令的性能优化策略
跳转指令的性能优化主要集中在减少分支预测错误和提高指令缓存命中率。ART采用了多种技术来实现这些优化。
对于条件跳转指令,ART通过分析程序的执行模式,提供分支预测提示,帮助处理器更准确地预测跳转结果。例如,在JIT编译过程中,ART会收集方法的执行统计信息,分析条件跳转指令的历史执行情况,然后根据这些信息设置分支预测提示:
cpp
// 设置分支预测提示的代码
void JitCompiler::SetBranchPredictionHint(HInstruction* instruction, BranchPrediction prediction) {
// 检查指令是否为条件跳转指令
if (instruction->IsConditionalBranch()) {
HConditionalBranch* branch = instruction->AsConditionalBranch();
// 根据预测结果设置相应的机器码属性
if (prediction == kLikelyTrue) {
branch->SetPrediction(BranchPrediction::kLikelyTaken);
} else if (prediction == kLikelyFalse) {
branch->SetPrediction(BranchPrediction::kLikelyNotTaken);
}
}
}
上述代码中,SetBranchPredictionHint
函数检查指令是否为条件跳转指令,如果是,则根据预测结果设置相应的分支预测属性。处理器在执行这些指令时,可以利用这些提示更准确地预测跳转结果,减少流水线停顿,提高执行效率。
ART还通过指令重排序和代码布局优化,提高跳转指令的性能。例如,将经常一起执行的代码块放在相邻的内存位置,减少指令缓存失效的次数。在方法内联优化中,ART会分析方法调用和跳转指令的模式,决定是否将被调用方法的代码内联到调用者中,从而减少方法调用和跳转的开销:
cpp
// 方法内联优化的代码
bool JitCompiler::TryInlineMethod(HInvoke* invoke) {
// 检查是否满足内联条件
if (!CanInlineMethod(invoke->GetTargetMethod())) {
return false;
}
// 获取被调用方法的IR图
HGraph* callee_graph = GetMethodGraph(invoke->GetTargetMethod());
if (callee_graph == nullptr) {
return false;
}
// 执行内联
InlineMethod(invoke, callee_graph);
// 优化内联后的代码
OptimizeInlinedCode(invoke->GetBlock()->GetGraph());
return true;
}
上述代码中,TryInlineMethod
函数首先检查被调用方法是否满足内联条件,如方法大小、调用频率等。如果满足条件,则获取被调用方法的中间表示(IR)图,并将其内联到调用者的代码中。内联后,还会对代码进行进一步优化,消除冗余操作和跳转指令,提高执行效率。
十一、异常处理与跳转指令的安全考量
11.1 异常处理的安全风险与防范
异常处理机制在提供强大功能的同时,也可能引入安全风险。例如,异常信息可能包含敏感信息,如果这些信息被泄露,可能导致安全漏洞。ART通过多种方式来防范这些风险。
首先,ART对异常信息进行严格控制,避免泄露敏感信息。当创建异常对象时,会对异常消息进行过滤,确保不包含敏感信息:
cpp
// 设置异常消息的安全过滤
void ExceptionHandler::SetExceptionMessage(ScopedObjectAccess& soa,
mirror::Throwable* exception,
const char* message) {
// 检查消息是否包含敏感信息
if (ContainsSensitiveInformation(message)) {
// 使用通用消息代替敏感消息
message = "An error occurred";
}
// 设置异常消息
exception->SetMessage(soa, soa.Env()->NewStringUTF(message));
}
上述代码中,SetExceptionMessage
函数在设置异常消息之前,会检查消息是否包含敏感信息。如果包含,则使用通用消息代替,从而避免敏感信息泄露。
ART还通过权限控制来限制异常处理代码的执行。某些异常处理操作需要特定的权限才能执行,例如访问系统资源或修改关键状态。ART在执行这些操作前,会检查调用者是否具有相应的权限:
cpp
// 执行敏感异常处理操作前的权限检查
bool ExceptionHandler::CheckPermissionForSensitiveOperation(Thread* thread) {
// 获取当前线程的访问权限
AccessFlags flags = thread->GetAccessFlags();
// 检查是否具有执行敏感操作的权限
if (!flags.HasPermission(Permission::kHandleCriticalExceptions)) {
// 记录安全事件
LogSecurityEvent("Attempt to handle critical exception without permission");
return false;
}
return true;
}
上述代码中,CheckPermissionForSensitiveOperation
函数检查当前线程是否具有处理关键异常的权限。如果没有权限,则记录安全事件并拒绝执行操作,从而防止未授权的异常处理代码执行敏感操作。
11.2 跳转指令的安全风险与防范
跳转指令也可能引入安全风险,例如恶意代码可能通过伪造跳转指令来改变程序的执行流程,从而实现代码注入或其他攻击。ART通过多种安全机制来防范这些风险。
首先,ART对跳转指令的目标地址进行严格验证,确保跳转到合法的代码位置。在解释执行模式下,解释器会检查跳转目标地址是否在合法的指令范围内:
cpp
// 验证跳转目标地址的代码
bool Interpreter::VerifyJumpTarget(uint16_t* target_pc, uint16_t* method_start) {
// 计算目标地址相对于方法起始地址的偏移
ptrdiff_t offset = target_pc - method_start;
// 获取方法的代码大小
size_t code_size = GetMethodCodeSize(method_start);
// 检查目标地址是否在合法范围内
if (offset < 0 || static_cast<size_t>(offset) >= code_size) {
// 非法跳转目标,记录安全事件
LogSecurityEvent("Invalid jump target detected");
return false;
}
return true;
}
上述代码中,VerifyJumpTarget
函数计算跳转目标地址相对于方法起始地址的偏移,并检查该偏移是否在合法范围内。如果不在合法范围内,则记录安全事件并拒绝执行跳转,从而防止恶意跳转。
在JIT编译和AOT编译模式下,编译器会生成验证代码,确保生成的机器码中的跳转指令指向合法的地址。此外,ART还利用内存保护机制,防止恶意代码修改跳转指令或目标地址。例如,将代码段设置为只读,防止运行时修改:
cpp
// 设置代码段为只读的代码
bool Compiler::ProtectCodeSection(void* code_start, size_t code_size) {
// 使用系统调用设置内存区域为只读
if (mprotect(code_start, code_size, PROT_READ) != 0) {
LOG(ERROR) << "Failed to set code section to read-only";
return false;
}
return true;
}
上述代码中,ProtectCodeSection
函数使用mprotect
系统调用将代码段设置为只读,防止运行时对代码进行修改。这样,即使恶意代码试图修改跳转指令或目标地址,也会因为权限不足而失败,从而保障了程序的安全性。
十二、异常处理与跳转指令的调试与诊断
12.1 调试工具对异常处理的支持
调试工具在开发过程中扮演着重要角色,对于异常处理和跳转指令的调试,ART提供了丰富的支持。例如,Android Studio的调试器可以帮助开发者定位和分析异常。
ART通过在异常对象中记录详细的堆栈跟踪信息,帮助开发者快速定位异常发生的位置。当异常发生时,ART会收集当前的调用栈信息,并将其保存在异常对象中:
cpp
// 收集堆栈跟踪信息的代码
void ExceptionHandler::FillInStackTrace(ScopedObjectAccess& soa, mirror::Throwable* exception) {
// 创建堆栈跟踪元素数组
mirror::ObjectArray<mirror::StackTraceElement>* stack_trace =
soa.Env()->NewObjectArray(EstimateStackTraceDepth(),
soa.Env()->FindClass("java/lang/StackTraceElement"));
// 获取当前线程的调用栈
Thread* thread = soa.Self();
ShadowFrame* shadow_frame = thread->TopShadowFrame();
// 遍历调用栈,收集堆栈跟踪元素
uint32_t depth = 0;
while (shadow_frame != nullptr && depth < stack_trace->GetLength()) {
// 创建堆栈跟踪元素
mirror::StackTraceElement* element = CreateStackTraceElement(soa, shadow_frame);
// 将元素添加到堆栈跟踪数组中
stack_trace->Set(depth++, element);
// 移动到上一个栈帧
shadow_frame = shadow_frame->GetLink();
}
// 设置异常的堆栈跟踪信息
exception->SetStackTrace(soa, stack_trace);
}
上述代码中,FillInStackTrace
函数首先创建一个StackTraceElement
数组,然后遍历当前线程的调用栈,为每个栈帧创建一个StackTraceElement
对象,记录方法名、类名、文件名和行号等信息。最后将这些元素数组设置到异常对象中,开发者在调试时可以通过异常对象获取完整的堆栈跟踪信息,快速定位异常发生的位置。
Android Studio的调试器还提供了异常断点功能,允许开发者在特定异常发生时暂停程序执行。这一功能依赖于ART提供的异常处理钩子:
cpp
// 设置异常断点的代码
void Debugger::SetExceptionBreakpoint(const char* exception_class_name, bool enabled) {
// 查找异常类
mirror::Class* exception_class = FindClassByName(exception_class_name);
if (exception_class == nullptr) {
LOG(WARNING) << "Exception class not found: " << exception_class_name;
return;
}
// 创建或更新异常断点
ExceptionBreakpoint* breakpoint = FindOrCreateExceptionBreakpoint(exception_class);
breakpoint->SetEnabled(enabled);
}
上述代码中,SetExceptionBreakpoint
函数允许调试器设置特定异常的断点。当指定的异常被抛出时,ART会检查是否设置了相应的断点,如果设置了,则暂停程序执行,允许开发者进行调试。
12.2 诊断工具对跳转指令的分析
对于跳转指令的分析,ART提供了多种诊断工具。例如,通过分析应用的执行日志和性能数据,可以了解跳转指令的执行情况和性能影响。
ART的性能分析工具可以收集跳转指令的执行统计信息,包括跳转次数、跳转类型和分支预测准确率等。这些信息可以帮助开发者优化代码,减少不必要的跳转和提高分支预测准确率:
cpp
// 收集跳转指令统计信息的代码
void Profiler::CollectBranchStatistics(Thread* thread, uint16_t* pc, bool taken) {
// 获取当前方法
ArtMethod* method = thread->GetCurrentMethod();
// 计算PC相对于方法起始地址的偏移
uint32_t offset = pc - method->GetEntryPointFromInterpreter();
// 更新分支统计信息
BranchStatistics* stats = GetOrCreateBranchStatistics(method, offset);
stats->IncrementCount();
if (taken) {
stats->IncrementTakenCount();
}
// 计算分支预测准确率
stats->CalculatePredictionAccuracy();
}
上述代码中,CollectBranchStatistics
函数在每次执行跳转指令时被调用,收集跳转指令的执行信息,包括跳转次数和跳转是否被执行。通过这些信息,可以计算分支预测准确率,帮助开发者了解哪些跳转指令的预测准确率较低,从而进行针对性的优化。
ART还提供了反汇编工具,允许开发者查看应用的字节码和机器码,分析跳转指令的具体实现和目标地址。这对于调试和优化跳转指令非常有帮助:
cpp
// 反汇编跳转指令的代码
void Disassembler::DisassembleBranchInstruction(uint16_t* pc, ArtMethod* method) {
// 获取指令操作码
uint16_t opcode = *pc;
// 解析跳转指令
BranchInstructionInfo info = ParseBranchInstruction(opcode, pc);
// 计算目标地址
uint16_t* target_pc = pc + info.offset;
// 输出反汇编信息
LOG(INFO) << "Branch instruction at 0x" << std::hex << (pc - method->GetEntryPointFromInterpreter())
<< ": " << GetInstructionName(opcode)
<< " -> target offset 0x" << std::hex << info.offset
<< ", target address 0x" << std::hex << (target_pc - method->GetEntryPointFromInterpreter());
}
上述代码中,DisassembleBranchInstruction
函数对跳转指令进行反汇编,解析指令的操作码和跳转偏移量,计算目标地址,并输出详细的反汇编信息。开发者可以利用这些信息分析跳转指令的执行逻辑和目标地址,找出可能存在的问题并进行优化。
十三、异常处理与跳转指令在不同Android版本中的演进
13.1 Android 5.0 (Lollipop) 中的实现
Android 5.0是首个正式使用ART运行时的版本,其异常处理和跳转指令的实现相对基础,但已经奠定了现代ART的基础架构。
在异常处理方面,Android 5.0的ART实现了基本的异常传播和处理机制。当异常发生时,ART会暂停当前线程,创建异常对象,并在调用栈中查找合适的异常处理代码。异常表的查找采用线性遍历方式,效率相对较低:
cpp
// Android 5.0中异常表查找的实现
uint32_t ArtMethod::FindExceptionHandlerPc(uint32_t dex_pc, mirror::Class* exception_class) {
// 获取方法的异常表
const DexFile::CodeItem* code_item = GetCodeItem();
if (code_item == nullptr || code_item->tries_size_ == 0) {
return kInvalidDexPc;
}
// 获取异常表
const DexFile::TryItem* tries = GetTryItems(code_item);
const uint8_t* handlers = GetExceptionHandlers(code_item);
// 线性遍历异常表
for (uint32_t i = 0; i < code_item->tries_size_; ++i) {
const DexFile::TryItem& try_item = tries[i];
if (dex_pc >= try_item.start_addr_ && dex_pc < try_item.start_addr_ + try_item.insn_count_) {
// 找到匹配的try块,查找对应的catch块
uint32_t handler_pc = FindMatchingHandler(handlers, try_item, exception_class);
if (handler_pc != kInvalidDexPc) {
return handler_pc;
}
}
}
return kInvalidDexPc;
}
上述代码展示了Android 5.0中异常表的查找方式,采用简单的线性遍历,逐个检查每个try
块是否包含异常发生的PC值。这种方法在处理包含大量try
块的方法时效率较低。
在跳转指令方面,Android 5.0的解释器实现了基本的跳转指令处理逻辑。例如,条件跳转指令会根据操作数栈中的值进行条件判断,并更新PC值:
cpp
// Android 5.0中条件跳转指令的处理
static void ExecuteIfEq(Thread* self, ShadowFrame* shadow_frame, uint16_t*& current_pc) {
// 从操作数栈弹出两个值
uint32_t value2 = shadow_frame->PopInt();
uint32_t value1 = shadow_frame->PopInt();
cpp
// 进行相等比较
if (value1 == value2) {
// 计算跳转偏移量
int16_t offset = DecodeSignedLeb128(¤t_pc);
// 更新PC值,实现跳转
current_pc += offset;
} else {
// 条件不成立,继续执行下一条指令
current_pc++;
}
}
此代码通过简单的比较和偏移计算实现条件跳转,未对性能优化做过多处理。在JIT编译方面,Android 5.0的JIT编译器初步支持热点代码编译,但对跳转指令的优化手段有限,主要集中在基础的代码生成,缺乏分支预测等高级优化策略。
13.2 Android 7.0 (Nougat) 中的改进
Android 7.0对ART的异常处理和跳转指令进行了多方面优化。在异常处理上,引入了更高效的异常表查找算法。通过对异常表进行预排序,将try
块按照起始地址升序排列,在查找异常处理代码时采用二分查找,显著提升了查找效率:
cpp
// Android 7.0中优化后的异常表查找
uint32_t ArtMethod::FindExceptionHandlerPcOptimized(uint32_t dex_pc, mirror::Class* exception_class) {
const DexFile::CodeItem* code_item = GetCodeItem();
if (code_item == nullptr || code_item->tries_size_ == 0) {
return kInvalidDexPc;
}
const DexFile::TryItem* tries = GetTryItems(code_item);
const uint8_t* handlers = GetExceptionHandlers(code_item);
// 二分查找匹配的try块
uint32_t left = 0;
uint32_t right = code_item->tries_size_ - 1;
while (left <= right) {
uint32_t mid = left + (right - left) / 2;
const DexFile::TryItem& try_item = tries[mid];
if (dex_pc < try_item.start_addr_) {
right = mid - 1;
} else if (dex_pc >= try_item.start_addr_ + try_item.insn_count_) {
left = mid + 1;
} else {
// 找到匹配try块,查找catch块
uint32_t handler_pc = FindMatchingHandler(handlers, try_item, exception_class);
if (handler_pc != kInvalidDexPc) {
return handler_pc;
}
break;
}
}
return kInvalidDexPc;
}
对于跳转指令,Android 7.0的JIT编译器开始引入分支预测机制。编译器根据代码执行的历史信息,为条件跳转指令添加分支预测提示,帮助处理器更准确地预测跳转方向,减少流水线停顿:
cpp
// Android 7.0中JIT编译器设置分支预测提示
void JitCompiler::SetBranchPredictionHint(HInstruction* instruction, BranchPrediction prediction) {
if (instruction->IsConditionalBranch()) {
HConditionalBranch* branch = instruction->AsConditionalBranch();
if (prediction == kLikelyTrue) {
branch->SetPrediction(BranchPrediction::kLikelyTaken);
} else if (prediction == kLikelyFalse) {
branch->SetPrediction(BranchPrediction::kLikelyNotTaken);
}
}
}
在解释器层面,对跳转指令的执行逻辑进行了优化,减少了指令执行的开销。例如,通过缓存部分计算结果,避免重复计算,提高了跳转指令的执行速度。
13.3 Android 10中的进一步优化
Android 10在异常处理和跳转指令上又有新的突破。在异常处理方面,增强了异常信息的安全性。对异常消息进行更严格的过滤和脱敏处理,防止敏感信息泄露。同时,优化了异常处理过程中的资源管理,减少了因异常处理导致的内存开销和性能损耗。
cpp
// Android 10中异常消息的安全过滤
void ExceptionHandler::SetExceptionMessage(ScopedObjectAccess& soa,
mirror::Throwable* exception,
const char* message) {
// 更严格的敏感信息检测规则
if (IsSensitiveMessage(message)) {
// 使用安全的默认消息
message = "An unexpected error occurred";
}
exception->SetMessage(soa, soa.Env()->NewStringUTF(message));
}
对于跳转指令,Android 10的JIT编译器进一步优化了分支预测算法,采用机器学习模型分析代码执行模式,动态调整分支预测策略,提高预测准确率。同时,加强了对间接跳转指令(如虚方法调用中的动态方法查找后的跳转)的优化,通过缓存方法查找结果,减少重复查找带来的开销:
cpp
// Android 10中JIT编译器的动态分支预测
void JitCompiler::DynamicBranchPrediction(HInstruction* instruction) {
if (instruction->IsConditionalBranch()) {
// 使用机器学习模型预测分支走向
BranchPrediction prediction = PredictBranchWithML(instruction);
HConditionalBranch* branch = instruction->AsConditionalBranch();
branch->SetPrediction(prediction);
}
}
// Android 10中虚方法调用的优化
ArtMethod* ArtMethod::ResolveVirtualMethodOptimized(ObjPtr<mirror::Object> receiver, uint32_t method_idx) {
// 尝试从缓存中获取方法
ArtMethod* cached_method = GetCachedVirtualMethod(receiver, method_idx);
if (cached_method != nullptr) {
return cached_method;
}
// 常规方法查找
mirror::Class* receiver_class = receiver->GetClass();
ArtMethod* method = receiver_class->FindVirtualMethod(method_idx);
if (method != nullptr) {
// 缓存查找结果
CacheVirtualMethod(receiver, method_idx, method);
return method;
}
// 父类中查找
mirror::Class* super_class = receiver_class->GetSuperClass();
while (super_class != nullptr) {
method = super_class->FindVirtualMethod(method_idx);
if (method != nullptr) {
CacheVirtualMethod(receiver, method_idx, method);
return method;
}
super_class = super_class->GetSuperClass();
}
ThrowNoSuchMethodError(receiver_class, method_idx);
return nullptr;
}
此外,Android 10还优化了代码布局,将频繁执行的代码块和跳转目标代码块放置在相邻内存区域,提高指令缓存命中率,进一步提升跳转指令执行性能。
十四、与其他运行时系统对比分析
14.1 与Java虚拟机(JVM)的差异
ART与Java虚拟机(JVM)在异常处理和跳转指令实现上存在诸多差异。在异常处理方面,JVM采用基于栈的异常处理模型,通过异常表和异常句柄来处理异常。当异常抛出时,JVM会从当前方法的栈帧开始,沿着调用栈向上查找匹配的异常句柄。与ART不同的是,JVM的异常表存储在方法的字节码属性中,查找方式在早期版本多为线性查找,虽然在后续版本引入了一些优化,但整体查找效率提升有限。
cpp
// JVM中简化的异常表查找示例
// 假设Method结构体包含异常表信息
struct Method {
ExceptionTableEntry* exceptionTable;
int exceptionTableSize;
};
void* findExceptionHandler(Method* method, int pc, ExceptionObject* exception) {
for (int i = 0; i < method->exceptionTableSize; i++) {
ExceptionTableEntry* entry = &method->exceptionTable[i];
if (pc >= entry->start_pc && pc < entry->end_pc) {
if (isExceptionMatch(exception, entry->exception_type)) {
return entry->handler_pc;
}
}
}
return nullptr;
}
在跳转指令方面,JVM的字节码指令集设计与ART的字节码指令集有所不同。JVM的跳转指令更侧重于Java语言特性的实现,例如goto
指令在JVM字节码中有多种变体,以适应不同的跳转场景。JVM的即时编译器(如HotSpot的C1、C2编译器)在跳转指令优化上采用了与ART不同的策略,HotSpot编译器更注重动态编译的适应性和优化深度,通过分层编译、逃逸分析等技术对跳转指令进行优化,而ART则更强调AOT编译阶段对跳转指令的优化,以减少运行时开销。
14.2 与JavaScript引擎(如V8)的对比
JavaScript引擎(以V8为例)与ART在异常处理和跳转指令实现上也有显著区别。在异常处理方面,V8引擎的异常处理机制与JavaScript语言的动态特性紧密相关。V8采用基于范围的异常处理,通过try - catch - finally
语句块来捕获和处理异常。当异常抛出时,V8会在调用栈中查找匹配的catch
块,查找过程依赖于函数调用栈的结构和作用域链。
cpp
// V8中简化的异常处理查找示例
// 假设Context结构体表示执行上下文
struct Context {
Function* function;
Context* parent;
};
void* findExceptionHandler(Context* context, Exception* exception) {
while (context != nullptr) {
Function* func = context->function;
if (func->hasExceptionHandler()) {
if (func->canHandleException(exception)) {
return func->getExceptionHandler();
}
}
context = context->parent;
}
return nullptr;
}
在跳转指令方面,V8引擎处理的是JavaScript代码编译后的机器码或字节码(在解释执行时)。由于JavaScript语言的动态特性,V8的跳转指令涉及到更多的动态类型检查和运行时决策。例如,函数调用和条件跳转可能需要在运行时根据对象的实际类型进行判断。V8通过内联缓存(Inline Caching)等技术优化函数调用和跳转指令的执行,这与ART针对Java语言静态类型特性的优化策略截然不同。
14.3 差异带来的影响与启示
ART与JVM、V8在异常处理和跳转指令实现上的差异,主要源于各自所服务语言的特性和运行时环境的需求。这些差异带来了不同的性能表现和应用场景。例如,ART的AOT编译和针对移动设备的优化,使其在Android应用运行时具有较好的启动速度和执行效率;JVM的动态编译和强大的优化能力,适合运行复杂的企业级Java应用;V8引擎的动态特性和高效的即时编译,使其在Web应用和JavaScript相关开发中表现出色。
从这些差异中可以得到启示,在设计和优化运行时系统时,需要充分考虑目标语言的特性、应用场景以及硬件环境。例如,对于强调启动速度的应用场景,可以借鉴ART的AOT编译和提前优化跳转指令的策略;对于动态性要求高的语言,则需要像V8一样设计灵活的异常处理和跳转指令执行机制,以适应运行时的动态变化。同时,不同运行时系统之间的优化技术也可以相互借鉴,如将机器学习用于分支预测的方法,在ART和V8中都有应用,未来可以进一步探索更通用的优化方案。