码字不易,辛苦大佬们点点关注,谢谢~
一、Android Runtime即时编译概述
Android Runtime(ART)中的即时编译(JIT,Just-In-Time Compilation)是提升应用执行效率的核心机制之一。与传统的解释执行方式不同,JIT能够在应用运行过程中,将频繁执行的代码(热点代码)动态编译为本地机器码,从而显著降低执行开销。在Android系统从Dalvik虚拟机过渡到ART后,JIT编译技术的引入大幅改善了应用的性能表现,尤其是在长时间运行的复杂应用场景中。
ART的JIT编译并非对所有代码进行一次性编译,而是依据特定的触发条件和阈值,选择性地对热点代码进行优化。这种策略既能避免全量编译带来的资源浪费,又能精准提升关键代码的执行效率。JIT编译触发条件与阈值的设定,直接影响着系统的资源分配和性能优化效果,是理解ART运行机制的关键环节。从源码层面看,这些条件和阈值的判定逻辑涉及多个模块的协同工作,包括热点代码探测、方法执行统计、系统资源监控等。
二、JIT编译触发的基础条件
2.1 方法调用次数达标
方法调用次数是最基础的JIT编译触发条件之一,相关逻辑主要实现在art/runtime/method.cc
和art/runtime/jit/jit_compiler.cc
文件中。在ArtMethod
类中,维护着方法的调用计数:
cpp
class ArtMethod {
private:
// 记录方法的调用次数
std::atomic<int> invocation_count_;
public:
// 获取方法的调用次数
int GetInvocationCount() const {
return invocation_count_.load(std::memory_order_relaxed);
}
// 增加方法调用计数
void IncrementInvocationCount() {
invocation_count_.fetch_add(1, std::memory_order_relaxed);
}
};
当方法被调用时,IncrementInvocationCount
函数会更新调用计数。在jit_compiler.cc
中,JIT编译器会检查方法调用次数是否达到阈值:
cpp
bool JitCompiler::CanCompile(ArtMethod* method) {
// 获取热点方法调用次数阈值,该阈值可动态调整
int hot_threshold = GetHotMethodInvocationThreshold();
if (method->GetInvocationCount() >= hot_threshold) {
return true;
}
return false;
}
只有当方法调用次数超过hot_threshold
时,才有可能触发JIT编译。这个阈值并非固定不变,系统会根据设备资源和运行状态动态调整,例如在设备资源紧张时提高阈值,减少编译开销。
2.2 方法执行时间占比超限
除了调用次数,方法的执行时间占比也是关键条件。ART通过记录方法每次执行的开始和结束时间,计算其总执行时长,并与整个应用的运行时间对比。在art/runtime/runtime.cc
中,存在方法执行时间统计的相关逻辑:
cpp
void Runtime::MeasureMethodExecutionTime(ArtMethod* method, uint64_t start_time, uint64_t end_time) {
// 计算方法执行时长
uint64_t duration = end_time - start_time;
// 获取或创建方法的执行时间统计对象
MethodExecutionStats* stats = GetMethodExecutionStats(method);
stats->total_time += duration;
stats->call_count++;
// 计算方法执行时间占比
double execution_percentage = (double)stats->total_time / GetTotalRuntime();
if (execution_percentage >= kExecutionTimeThreshold) {
// 标记方法为热点,触发JIT编译检查
method->SetIsHot(true);
}
}
当方法的执行时间占比超过kExecutionTimeThreshold
时,该方法会被标记为热点,进而进入JIT编译的候选队列。这种机制确保了即使调用次数不多,但执行耗时较长的方法也能得到优化。
2.3 代码结构符合编译要求
并非所有满足调用次数或执行时间条件的方法都能触发JIT编译,代码结构同样需要符合要求。例如,方法的字节码大小不能超过限制,以避免编译时间过长或生成的机器码占用过多内存。在jit_compiler.cc
中存在如下限制逻辑:
cpp
bool JitCompiler::CanCompile(ArtMethod* method) {
// 最大允许编译的字节码大小,单位为字节
const size_t max_bytecode_size = kMaxJitCompileBytecodeSize;
if (method->GetBytecodeSize() > max_bytecode_size) {
return false;
}
// 其他条件检查...
return true;
}
此外,递归深度过高、包含复杂动态特性(如反射调用过多)的方法,也可能被排除在JIT编译之外,因为这些情况会增加编译的复杂度和不确定性。
三、热点代码的动态识别与标记
3.1 运行时插桩与数据采集
为了准确识别热点代码,ART在字节码解释执行阶段进行运行时插桩。在art/runtime/interpreter/interpreter_common.cc
中,方法调用、循环跳转等关键位置都插入了统计代码。以方法调用为例:
cpp
void Interpreter::InvokeVirtual(Thread* self, const DexFile::CodeItem* code_item,
const ShadowFrame& shadow_frame) {
// 记录方法调用开始时间
uint64_t start_time = GetCurrentTime();
ArtMethod* method = GetMethodFromInvokeInstruction(/* 相关参数 */);
method->IncrementInvocationCount();
// 执行方法调用逻辑...
// 记录方法调用结束时间
uint64_t end_time = GetCurrentTime();
// 上报方法执行时间统计
Runtime::MeasureMethodExecutionTime(method, start_time, end_time);
}
这些插桩代码在不影响原有逻辑的前提下,实时采集方法调用次数、执行时长等数据,为热点代码的识别提供依据。
3.2 热点标记的状态机实现
ART通过状态机机制管理方法的热点状态,相关代码分布在art/runtime/method_state_machine.cc
中。方法的状态包括kNotHot
(非热点)、kWarm
(预热状态)、kHot
(热点)等:
cpp
enum class MethodState {
kNotHot,
kWarm,
kHot
};
class MethodStateMachine {
private:
MethodState state_;
public:
void UpdateState(ArtMethod* method) {
int count = method->GetInvocationCount();
if (state_ == MethodState::kNotHot && count >= kWarmThreshold) {
// 达到预热阈值,进入预热状态
state_ = MethodState::kWarm;
} else if (state_ == MethodState::kWarm && count >= kHotThreshold) {
// 达到热点阈值,标记为热点
state_ = MethodState::kHot;
method->SetIsHot(true);
}
}
};
方法状态会随着调用次数的增加逐步升级,只有进入kHot
状态的方法,才会真正触发JIT编译流程。这种分级机制避免了频繁编译边缘热点,提高了资源利用效率。
3.3 循环热点的特殊处理
循环代码由于重复执行特性,是热点代码的重要来源。ART在art/compiler/optimizing/loop_analysis.cc
中专门处理循环热点:
cpp
void LoopAnalysis::AnalyzeLoop(HLoop* loop) {
// 计算循环的执行次数估计值
int estimated_iterations = EstimateLoopIterations(loop);
if (estimated_iterations >= kLoopHotThreshold) {
// 标记循环内的方法为热点
MarkLoopMethodsAsHot(loop);
}
// 分析循环不变量、分支预测等...
}
通过静态分析循环结构和动态统计执行次数,ART能够识别出高频循环,并将其内部代码优先纳入JIT编译范围。这种策略对于提升包含大量循环的算法类代码性能尤为有效。
四、JIT编译阈值的动态调整策略
4.1 基于系统资源的阈值调节
ART会根据设备的CPU、内存等资源使用情况,动态调整JIT编译阈值。在art/runtime/device_config.cc
中实现了相关逻辑:
cpp
class DeviceConfig {
public:
void AdjustJitThresholds() {
// 获取CPU使用率
float cpu_usage = GetCpuUsage();
// 获取内存剩余量
size_t free_memory = GetFreeMemory();
if (cpu_usage > kHighCpuThreshold || free_memory < kLowMemoryThreshold) {
// 资源紧张时提高热点阈值
IncreaseHotThreshold();
// 降低编译频率
DecreaseCompilationFrequency();
} else if (cpu_usage < kLowCpuThreshold && free_memory > kHighMemoryThreshold) {
// 资源充裕时降低热点阈值
DecreaseHotThreshold();
// 提高编译频率
IncreaseCompilationFrequency();
}
}
};
当CPU负载过高或内存不足时,系统会提高热点阈值,减少JIT编译任务;反之,则降低阈值,更积极地优化代码。这种动态调节机制确保了系统在不同资源条件下都能维持性能与资源消耗的平衡。
4.2 应用生命周期感知的阈值变化
在应用的不同生命周期阶段,JIT编译阈值也会相应调整。在art/runtime/application_lifecycle.cc
中:
cpp
void ApplicationLifecycle::OnApplicationStart() {
// 应用启动阶段降低阈值,加速初始化
SetHotThreshold(kInitialStartupThreshold);
// 优先编译启动关键路径上的方法
PrioritizeStartupMethodsForCompilation();
}
void ApplicationLifecycle::OnApplicationEnterBackground() {
// 应用后台运行时提高阈值,节省资源
SetHotThreshold(kBackgroundThreshold);
// 暂停非紧急的编译任务
PauseNonCriticalCompilations();
}
void ApplicationLifecycle::OnApplicationResume() {
// 应用恢复前台时恢复正常阈值
SetHotThreshold(kNormalThreshold);
// 重启被暂停的编译任务
ResumeCompilations();
}
通过感知应用的启动、后台、恢复等状态,ART能够针对性地调整编译策略,在保证用户体验的同时合理分配系统资源。
4.3 基于历史数据的自适应学习
较新版本的ART引入了基于历史数据的自适应机制,在art/runtime/hot_code_prediction.cc
中实现:
cpp
class HotCodePredictor {
private:
// 存储历史热点方法的调用模式和编译效果
std::vector<HotCodeRecord> history_;
public:
void UpdateHistory(ArtMethod* method, bool is_compiled, bool performance_improved) {
HotCodeRecord record;
record.method = method;
record.is_compiled = is_compiled;
record.performance_improvement = performance_improved;
history_.push_back(record);
}
void AdjustThresholdsBasedOnHistory() {
// 分析历史数据,例如统计编译后性能提升的方法特征
// 根据分析结果调整阈值,例如对相似特征的方法降低阈值
AnalyzeHistoryAndAdjustThresholds();
}
};
系统通过记录热点方法的编译效果,学习哪些类型的代码更适合编译,从而动态优化阈值设定,提升整体优化效率。
五、JIT编译触发的完整流程
5.1 触发条件的前置检查
当方法执行时,ART首先在解释器中进行基础检查。在interpreter_common.cc
的方法调用逻辑中:
cpp
void Interpreter::InvokeVirtual(Thread* self, const DexFile::CodeItem* code_item,
const ShadowFrame& shadow_frame) {
ArtMethod* method = GetMethodFromInvokeInstruction(/* 相关参数 */);
// 检查方法是否已被编译为本地代码,若已编译则直接执行
if (method->IsCompiled()) {
ExecuteCompiledCode(method);
return;
}
// 更新调用计数并检查是否达到热点阈值
method->IncrementInvocationCount();
if (method->IsHot()) {
// 进入JIT编译流程
EnqueueForJitCompilation(method);
} else {
// 解释执行方法
InterpretMethod(method);
}
}
如果方法尚未编译且已达到热点状态,会被加入JIT编译队列。
5.2 编译任务的排队与调度
JIT编译任务通过队列管理,相关实现位于art/runtime/jit/jit_compilation_queue.cc
:
cpp
class JitCompilationQueue {
private:
// 编译任务队列
std::queue<ArtMethod*> compilation_queue_;
std::mutex queue_mutex_;
public:
void Enqueue(ArtMethod* method) {
std::lock_guard<std::mutex> lock(queue_mutex_);
compilation_queue_.push(method);
}
ArtMethod* Dequeue() {
std::lock_guard<std::mutex> lock(queue_mutex_);
if (compilation_queue_.empty()) {
return nullptr;
}
ArtMethod* method = compilation_queue_.front();
compilation_queue_.pop();
return method;
}
};
JIT编译器会定期从队列中取出任务进行编译,同时考虑任务的优先级(如启动关键方法优先编译)。调度策略确保了编译资源优先分配给对应用性能影响最大的代码。
5.3 编译过程的执行与反馈
当编译任务被取出后,JIT编译器开始执行编译流程。在jit_compiler.cc
中:
cpp
void JitCompiler::CompileMethod(ArtMethod* method) {
// 生成中间表示(IR)
HGraph* graph = CreateHGraphForMethod(method);
// 执行前端优化,如常量折叠、死代码消除
PerformFrontendOptimizations(graph);
// 生成目标机器码
GenerateMachineCode(graph);
// 执行后端优化,如寄存器分配、指令调度
PerformBackendOptimizations();
// 将编译后的机器码替换原方法的执行逻辑
InstallCompiledCode(method);
// 反馈编译结果,例如更新方法状态、统计编译耗时
ReportCompilationResult(method, success);
}
编译完成后,系统会记录编译效果(如是否成功、性能提升幅度),并据此调整后续的编译策略和阈值设定。
六、不同Android版本的JIT触发机制演进
6.1 Android 5.0 - 7.0的基础实现
在Android 5.0首次引入ART时,JIT编译触发条件较为简单,主要依赖固定的方法调用次数阈值。早期art/runtime/jit/jit_compiler.cc
中的判定逻辑如下:
cpp
bool JitCompiler::CanCompile(ArtMethod* method) {
// 固定的热点方法调用次数阈值
const int fixed_threshold = 100;
if (method->GetInvocationCount() >= fixed_threshold) {
return true;
}
return false;
}
这一阶段缺乏动态调整机制,容易导致编译资源浪费或优化不足。随着版本迭代至Android 7.0,开始引入基于执行时间的触发条件,并初步实现了简单的资源感知阈值调节。
6.2 Android 8.0 - 10.0的功能完善
Android 8.0 - 10.0版本对JIT触发机制进行了大幅优化。在device_config.cc
中新增了动态阈值调节逻辑:
cpp
void DeviceConfig::AdjustJitThresholds() {
// 引入CPU负载感知
if (GetCpuLoad() > 0.8) {
IncreaseHotThresholdByPercentage(20);
}
// 增加内存使用情况判断
if (GetAvailableMemory() < 512 * 1024 * 1024) { // 512MB
DecreaseCompilationCacheSize();
}
}
同时,优化了循环热点的识别算法,在loop_analysis.cc
中加入了更精确的循环执行次数预估模型,提升了JIT编译的准确性和效率。
6.3 Android 11.0及以后的智能化升级
从Android 11.0开始,JIT触发机制向智能化方向演进。引入机器学习预测热点,在hot_code_prediction_ml.cc
中:
cpp
class HotCodeMLPredictor {
public:
void TrainModel() {
// 收集方法的特征数据(调用次数、执行时间、代码结构等)
std::vector<MethodFeature> features = CollectMethodFeatures();
// 使用神经网络模型进行训练
ml_model_.Train(features);
}
bool PredictHotCode(ArtMethod* method) {
MethodFeature feature = ExtractFeature(method);
// 通过模型预测是否为热点
return ml_model_.Predict(feature);
}
};
这种基于数据驱动的方式,使系统能够更精准地预判潜在热点,提前触发编译,进一步提升应用启动速度和运行流畅度。
七、JIT触发机制与其他模块的协同工作
7.1 与热点代码探测模块的联动
JIT编译的触发依赖于热点代码探测模块提供的数据支持。热点探测模块通过运行时插桩采集数据,在runtime_monitor.cc
中汇总并分析:
cpp
class RuntimeMonitor {
public:
void AnalyzeHotCodeCandidates() {
// 遍历所有方法,检查调用次数和执行时间
for (ArtMethod* method : GetAllMethods()) {
int
JIT编译的触发依赖于热点代码探测模块提供的数据支持。热点探测模块通过运行时插桩采集数据,在runtime_monitor.cc
中汇总并分析:
cpp
class RuntimeMonitor {
public:
void AnalyzeHotCodeCandidates() {
// 遍历所有方法,检查调用次数和执行时间
for (ArtMethod* method : GetAllMethods()) {
int invocationCount = method->GetInvocationCount();
uint64_t totalExecutionTime = GetTotalExecutionTimeOfMethod(method);
// 判断是否达到热点标记的基础条件
if (invocationCount >= kInvocationThreshold || totalExecutionTime >= kExecutionTimeThreshold) {
method->SetIsPotentialHot(true);
// 将潜在热点方法上报给JIT编译模块
NotifyJitModule(method);
}
}
}
private:
// 通知JIT模块有新的潜在热点方法
void NotifyJitModule(ArtMethod* method) {
JitCompilationQueue* queue = JitCompilationQueue::GetInstance();
queue->Enqueue(method);
}
};
热点探测模块将符合初步条件的方法推入JIT编译队列,而JIT模块在执行编译前,还会再次根据更严格的触发条件(如代码结构、系统资源状态)进行筛选,确保只对真正值得优化的代码进行编译。
7.2 与垃圾回收(GC)机制的交互
JIT编译生成的本地机器码在内存中占用空间,其生命周期需要与垃圾回收机制协同管理。在art/runtime/gc/heap.cc
中,垃圾回收器在执行回收操作时,会扫描JIT编译产生的代码区域:
cpp
void Heap::GcTraceJitCode() {
// 遍历JIT编译产生的代码内存区域
for (JitCode* jitCode : jitCodeList_) {
if (jitCode->IsLive()) {
// 如果代码仍在使用中,标记相关内存为存活
MarkLiveMemory(jitCode->GetMemoryRegion());
} else {
// 否则释放对应的内存资源
FreeJitCodeMemory(jitCode);
}
}
}
此外,当系统内存紧张时,垃圾回收机制会反馈给JIT模块,促使其调整编译策略。例如,减少新的JIT编译任务,或优先释放长时间未使用的JIT编译代码占用的内存,避免因编译导致内存溢出。
7.3 与AOT(提前编译)的协作
Android同时支持JIT即时编译与AOT提前编译,二者在提升应用性能上相互配合。在系统空闲时,AOT会对应用的代码进行全局性的预编译,生成.oat
文件。而JIT则专注于运行时动态识别的热点代码优化。在art/runtime/aot/aot_compiler.cc
与jit_compiler.cc
的协作逻辑中:
cpp
// AOT编译完成后,通知JIT模块更新编译策略
void AotCompiler::OnAotCompilationFinished() {
// 检查是否有需要JIT补充优化的部分
std::vector<ArtMethod*> methodsToJit = IdentifyMethodsForJit();
JitCompilationQueue* queue = JitCompilationQueue::GetInstance();
for (ArtMethod* method : methodsToJit) {
queue->Enqueue(method);
}
}
AOT编译可以减少JIT在应用启动阶段的工作量,而JIT则能针对AOT无法覆盖的动态热点代码进行即时优化,两者结合实现了编译效率与运行性能的平衡。
八、JIT编译触发的性能影响与权衡
8.1 编译过程的资源开销
JIT编译本身需要消耗CPU、内存等系统资源。在编译过程中,前端优化(如控制流分析、数据流分析)和后端优化(如寄存器分配、指令调度)都会占用大量计算资源。以art/compiler/optimizing/optimizing_compiler.cc
中的前端优化逻辑为例:
cpp
void OptimizingCompiler::PerformFrontendOptimizations(HGraph* graph) {
// 常量折叠优化
ConstantFolding(graph);
// 死代码消除
DeadCodeElimination(graph);
// 循环不变量提取
ExtractLoopInvariants(graph);
// 每一项优化操作都需要遍历代码图,计算开销较大
}
此外,编译生成的机器码需要存储在内存中,若频繁触发JIT编译,会导致内存占用快速增长。因此,ART必须通过合理的阈值设定和动态调整策略,控制编译任务的频率和规模,避免资源过度消耗。
8.2 优化带来的性能收益
尽管存在资源开销,但JIT编译对热点代码的优化能显著提升应用性能。以一个频繁调用的计算密集型方法为例,解释执行与JIT编译后的性能对比如下:
cpp
// 未编译的解释执行方式
void InterpretedCalculationMethod() {
int result = 0;
for (int i = 0; i < 1000000; ++i) {
result += ComplexCalculation(i);
}
}
// JIT编译后的本地机器码执行方式
void CompiledCalculationMethod() {
// 编译后的代码直接使用高效的机器指令
// 例如,循环展开、寄存器直接操作等优化
int result = 0;
// 假设循环展开后的内联代码
result += ComplexCalculation(0);
result += ComplexCalculation(1);
// ...
}
JIT编译通过内联优化、指令重排、消除冗余操作等手段,大幅减少了方法的执行时间。根据实测数据,热点方法经过JIT编译后,执行效率通常可提升2-10倍不等 。
8.3 触发阈值的平衡策略
ART通过动态调整JIT触发阈值,在编译开销与性能收益间寻找平衡。当系统资源充足时,降低阈值以积极触发编译,获取更高性能;而在资源紧张时(如多任务运行、电量不足),则提高阈值减少编译任务。在device_config.cc
中,动态调整逻辑如下:
cpp
void DeviceConfig::AdjustThresholdsBasedOnResource() {
// 获取当前CPU负载
float cpuLoad = GetCpuLoad();
// 获取剩余内存
size_t freeMemory = GetFreeMemory();
if (cpuLoad > 0.9 || freeMemory < 256 * 1024 * 1024) { // 256MB
// 资源紧张时,将热点调用次数阈值提高50%
hotInvocationThreshold_ *= 1.5;
// 延长编译任务间隔时间
compilationInterval_ *= 2;
} else if (cpuLoad < 0.3 && freeMemory > 1024 * 1024 * 1024) { // 1GB
// 资源充足时,降低阈值并缩短间隔
hotInvocationThreshold_ *= 0.8;
compilationInterval_ *= 0.5;
}
}
这种动态平衡策略确保了系统在不同场景下,既能有效提升应用性能,又不会因过度编译影响整体稳定性。
九、JIT触发机制的调试与监控
9.1 运行时状态监控接口
ART提供了一系列接口用于监控JIT编译的运行状态。在art/runtime/jit/jit_runtime_stats.cc
中,定义了获取编译统计数据的方法:
cpp
class JitRuntimeStats {
public:
// 获取当前JIT编译队列的任务数量
int GetCompilationQueueSize() {
return jitCompilationQueue_.Size();
}
// 获取已完成的JIT编译任务总数
int GetTotalCompiledMethods() {
return totalCompiledMethods_;
}
// 获取最近一次JIT编译的耗时
uint64_t GetLastCompilationDuration() {
return lastCompilationDuration_;
}
private:
JitCompilationQueue jitCompilationQueue_;
int totalCompiledMethods_;
uint64_t lastCompilationDuration_;
};
开发者或系统调试工具可通过这些接口,实时了解JIT编译的负载情况、任务进度等信息,为性能分析提供数据支持。
9.2 调试日志与追踪
ART的JIT模块支持详细的调试日志输出,帮助开发者定位编译过程中的问题。在jit_compiler.cc
中,通过日志开关控制输出内容:
cpp
void JitCompiler::CompileMethod(ArtMethod* method) {
if (JitLogging::IsEnabled()) {
LOG(INFO) << "Starting JIT compilation for method: " << method->PrettyName();
}
// 编译前检查
if (!CanCompile(method)) {
if (JitLogging::IsEnabled()) {
LOG(INFO) << "Method does not meet compilation criteria, skipped";
}
return;
}
// 编译过程...
if (JitLogging::IsEnabled()) {
LOG(INFO) << "Compilation finished, generated code size: " << generatedCodeSize_;
}
}
通过设置JitLogging
相关参数,可输出编译决策依据、优化步骤细节、错误信息等日志,便于分析编译失败原因或优化策略有效性。
9.3 性能分析工具集成
Android系统的性能分析工具(如Systrace、Perfetto)深度集成了对JIT编译的追踪功能。以Systrace为例,在art/tools/systrace/art_systrace.cc
中,定义了JIT事件的追踪逻辑:
cpp
void TraceJitCompilationStart(ArtMethod* method) {
// 向Systrace发送JIT编译开始事件
SystraceBegin("JIT_Compile", "method=%s", method->PrettyName());
}
void TraceJitCompilationEnd() {
// 发送编译结束事件
SystraceEnd("JIT_Compile");
}
这些工具可直观展示JIT编译的时间线、资源占用情况,以及编译对应用性能的影响,帮助开发者针对性地优化触发条件和阈值设定。