1 虚拟机执行入口
Android ART (Android Runtime) 从操作系统级别启动 Java 应用程序或服务的过程。
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
char* slashClassName = toSlashClassName(className != NULL ? className : "");
jclass startClass = env->FindClass(slashClassName);
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
过程概述
在 Android 系统中,当需要启动一个 Java 应用程序或服务时,操作系统首先需要创建一个运行环境,这通常是通过 zygote 进程实现的。Zygote 是一个特殊的守护进程,它预加载了 Android 运行时和常用类库,用于快速启动新的应用进程。一旦 zygote 准备好,它就会复制自身(通过 fork)来为新的应用创建一个进程。
AndroidRuntime::start 函数
这个函数是启动 Java 主类的关键。它接受一个类名作为参数,并通过 JNI 调用这个类的 main
方法。下面是这个过程的详细说明:
类名处理:
char* slashClassName = toSlashClassName(className != NULL ? className : "");
将点分隔的 Java 类名转换为以斜杠分隔的形式,因为 JNI 使用的是斜杠分隔的类路径。
查找类:
jclass startClass = env->FindClass(slashClassName);
使用 JNI 环境 env
查找指定的类。这个步骤涉及到类加载器,它会在应用的 APK 文件或其他存储位置查找并加载这个类。
获取方法 ID:
jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");
获取这个类的 main
方法的方法 ID。这里指定 main
方法必须是静态的,接受一个字符串数组参数,没有返回值(void
)。
调用 Java 方法:
env->CallStaticVoidMethod(startClass, startMeth, strArray);
调用 main
方法。strArray
是传递给 main
方法的参数,其内容应与 options
相对应。这一步是通过 JNI 桥接完成的。
JNI 到 ART 方法的调用
env->CallStaticVoidMethod
是 JNI 函数的一个指针,它指向 jni_internal.cc::CallStaticVoidMethodV()
,这个函数处理具体的方法调用逻辑。
调用 ArtMethod:
当 JNI 函数被调用时,它将进一步调用 ArtMethod
的 Invoke()
方法。Invoke()
是 ART 中的一个核心函数,负责执行方法调用。
汇编代码跳转:
Invoke()
方法会利用 entry_point_from_compiled_code_
进行跳转。这通常指向一个汇编代码 art_quick_invoke_stub_internal
。这段汇编代码负责将控制权转移给方法的实际执行代码(如果已经编译)或者解释器(如果代码尚未编译)。
2 LinkCode中设置ArtMethod入口点
在Java和Android的运行时环境中,类加载主要分为三个阶段:加载(Loading)、链接(Linking)、初始化(Initialization)。在链接阶段,
ClassLinker::LinkCode` 的作用主要集中在准备方法的代码执行,具体包括:
-
编译方法:
LinkCode
负责将方法的字节码编译成机器码。在ART中,这通常涉及到AOT编译(Ahead-of-Time),即在应用安装时就将字节码编译成机器代码,或者JIT编译(Just-In-Time),即在应用运行时编译。对于AOT,编译可能发生在应用的安装阶段或者通过后台编译服务。 -
生成直接引用:除了编译代码外,
LinkCode
还需要处理对类成员(如字段和方法)的直接引用。这意味着它将解析方法中使用的所有符号引用,将它们转换为直接引用,以便于在运行时提高执行效率。 -
方法入口地址设置:编译完成后,
LinkCode
会将方法的入口地址设置到方法表中的相应位置。这使得当运行时环境需要调用某个方法时,可以直接通过方法表找到其入口地址并执行。
在 ART 中,每一个函数都对应一个 ARTMethod 结构体。这个结构体包含了不同模式下的调用入口点:ArtMethod有如下2个入口地址
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_;
class ArtMethod {
struct PtrSizedFields {
void* data_;//与JNI有关
void* entry_point_from_quick_compiled_code_;//java方法入口函数地址
} ptr_sized_fields_;
};
其中entry_point_from_quick_compiled_code_保存的是非jni方法的入口地址,设置entry_point_from_compiled_code_是在Class_Linker.cc::LinkCode()完成的,指向一段Trampoline代码来间接执行。
if (method->IsStatic() && !method->IsConstructor()) {
method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
} else if (quick_code == nullptr && method->IsNative()) {
method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
} else if (enter_interpreter) {
method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
}
在 Android ART(Android Runtime)中,entry_point_from_quick_compiled_code_
是方法执行的入口点,它决定了如何执行一个方法。这个入口点根据方法的属性和状态选择不同的执行路径。有四种主要的执行路径入口,每种都适用于特定的场景。以下是每种情况的详细解释:
art_quick_resolution_trampoline
这个入口是用于静态方法的,但不包括构造方法。静态方法由于不依赖于类的具体实例来执行,因此它们在类被加载时就需要被解析和准备好。
- 条件:
method->IsStatic() && !method->IsConstructor()
- 使用场景:当一个静态方法被调用,但尚未被解析(即首次调用或者类刚刚被加载)时,这个入口将会被使用。它负责解析方法的符号引用并链接到具体的代码实现。
art_quick_generic_jni_trampoline
这是为 JNI 方法设置的入口点,用于那些通过 JNI 接口与本地代码交互的方法。如果这些方法没有预编译的机器代码(quick code),则使用这个入口。
- 条件:
quick_code == nullptr && method->IsNative()
- 使用场景:当 JNI 方法没有直接的编译代码时,这个入口负责设置 JNI 环境,并调用相应的本地方法。
art_quick_to_interpreter_bridge
这个入口用于解释执行的情况,通常用于那些不经常执行或者还未被 JIT 编译的方法。
- 条件:
enter_interpreter
,符合解释器执行的条件 - 使用场景:当方法不适合直接编译执行,或者编译执行的代价较高(如只执行一次的方法),或者代码仍在动态优化中,这个入口将被用来逐条解释执行字节码。
quick_code
如果以上条件都不满足,并且存在预编译的快速执行代码(quick code),则直接使用这些机器代码执行方法。
- 条件:其他条件都不符合,并且
oat_method
中有对应的 quick code - 使用场景:对于频繁执行的热点代码,ART 会通过 Ahead-Of-Time (AOT) 编译或 Just-In-Time (JIT) 编译生成优化的机器代码。这些代码直接运行在硬件上,提供最佳的执行性能。
3 执行
在 Android ART (Android Runtime) 中,虚拟机通过一系列的桥接代码和入口点来处理 Java 方法的调用。下面图的起始点为invoke
详见《深入理解Android:Java虚拟机》ART10.2 解释执行
3.1 Quick Code和Interpreter 模式
Quick Code 模式
执行 ARM 汇编指令
在这种模式下,Dalvik 字节码被提前(Ahead-of-Time, AOT)编译成 ARM 汇编指令。这意味着在应用安装时,字节码就已经被转换成了机器代码。
由于代码是预编译的,运行时的性能得到了显著提升,因为不需要在每次应用运行时都进行字节码到机器代码的转换。
Interpreter 模式
由解释器解释执行 Dalvik 字节码:
在这种模式下,Dalvik 字节码在运行时被逐条解释执行。这种方式不需要预编译,可以更快地开始执行应用,但运行速度相对较慢。
这种模式通常用于应用的调试过程中,因为它允许开发者更容易地跟踪和调试代码。
两种模式之间的交互
混合执行:即使是在 quick code 模式中,也有一些类方法可能需要以 Interpreter 模式执行,反之亦然。这种混合执行模式允许 ART 在保持高性能的同时,提供更多的灵活性。
函数桥接:
artInterpreterToCompiledCodeBridge: 这个函数允许从 Interpreter 模式切换到 quick code 模式。
GetQuickToInterpreterBridge: 这个函数允许从 quick code 模式切换到 Interpreter 模式。
如下/art/runtime/runtime.cc代码是检查运行时选项中是否设置了解释执行模式(Opt::Interpret)。如果设置了该选项,代码将调用 GetInstrumentation()->ForceInterpretOnly() 方法,强制 ART 进入只用解释执行的模式。
if (runtime_options.GetOrDefault(Opt::Interpret)) {
GetInstrumentation()->ForceInterpretOnly();
}
设置 ART 为解释执行模式
从上面的代码片段中可以看出,ART 的解释执行模式是通过检查启动选项 Opt::Interpret 来设置的。如果这个选项被设置为 true,那么 ART 会在整个运行期间强制使用解释器来执行所有的 Java 方法。
为了在实际使用中设置 ART 为解释执行模式,需要在启动 ART 时提供相应的选项。这通常可以通过命令行参数来实现,例如,在启动 Android 设备或启动特定的 Android 应用时,可以指定相关的运行时参数。
adb shell setprop debug.art.interpret-only true
或者在启动应用时指定相关的 VM 参数:
am start --es “android.vm.dex2oat-flags” “–interpreter-only” package.name/Activity
这些命令设置 ART 运行时在处理 Java 方法时只使用解释器,而不进行 JIT 或 AOT 编译
3.2 Execute函数
ART的解释执行主要在解释器部分处理,这通常在 interpreter 目录下的某个文件中实现,如 interpreter/interpreter_common.h 或 interpreter/interpreter.cc。ART 中的 ArtMethod 结构体包含了方法的所有元数据,包括方法名。可以通过这个结构体获取当前方法的名称。
如下函数体的核心功能是通过解释执行或 JIT 编译执行方法,同时处理相关的事件和异常。
249 NO_STACK_PROTECTOR
250 static inline JValue Execute(
251 Thread* self,
252 const CodeItemDataAccessor& accessor,
253 ShadowFrame& shadow_frame,
254 JValue result_register,
255 bool stay_in_interpreter = false,
256 bool from_deoptimize = false) REQUIRES_SHARED(Locks::mutator_lock_) {
257 DCHECK(!shadow_frame.GetMethod()->IsAbstract());
258 DCHECK(!shadow_frame.GetMethod()->IsNative());
259
260 // We cache the result of NeedsDexPcEvents in the shadow frame so we don't need to call
261 // NeedsDexPcEvents on every instruction for better performance. NeedsDexPcEvents only gets
262 // updated asynchronoulsy in a SuspendAll scope and any existing shadow frames are updated with
263 // new value. So it is safe to cache it here.
264 shadow_frame.SetNotifyDexPcMoveEvents(
265 Runtime::Current()->GetInstrumentation()->NeedsDexPcEvents(shadow_frame.GetMethod(), self));
266
267 if (LIKELY(!from_deoptimize)) { // Entering the method, but not via deoptimization.
268 if (kIsDebugBuild) {
269 CHECK_EQ(shadow_frame.GetDexPC(), 0u);
270 self->AssertNoPendingException();
271 }
272 ArtMethod *method = shadow_frame.GetMethod();
273
274 // If we can continue in JIT and have JITed code available execute JITed code.
275 if (!stay_in_interpreter && !self->IsForceInterpreter() && !shadow_frame.GetForcePopFrame()) {
276 jit::Jit* jit = Runtime::Current()->GetJit();
277 if (jit != nullptr) {
278 jit->MethodEntered(self, shadow_frame.GetMethod());
279 if (jit->CanInvokeCompiledCode(method)) {
280 JValue result;
281
282 // Pop the shadow frame before calling into compiled code.
283 self->PopShadowFrame();
284 // Calculate the offset of the first input reg. The input registers are in the high regs.
285 // It's ok to access the code item here since JIT code will have been touched by the
286 // interpreter and compiler already.
287 uint16_t arg_offset = accessor.RegistersSize() - accessor.InsSize();
288 ArtInterpreterToCompiledCodeBridge(self, nullptr, &shadow_frame, arg_offset, &result);
289 // Push the shadow frame back as the caller will expect it.
290 self->PushShadowFrame(&shadow_frame);
291
292 return result;
293 }
294 }
295 }
296
297 instrumentation::Instrumentation* instrumentation = Runtime::Current()->GetInstrumentation();
298 if (UNLIKELY(instrumentation->HasMethodEntryListeners() || shadow_frame.GetForcePopFrame())) {
299 instrumentation->MethodEnterEvent(self, method);
300 if (UNLIKELY(shadow_frame.GetForcePopFrame())) {
301 // The caller will retry this invoke or ignore the result. Just return immediately without
302 // any value.
303 DCHECK(Runtime::Current()->AreNonStandardExitsEnabled());
304 JValue ret = JValue();
305 PerformNonStandardReturn(self,
306 shadow_frame,
307 ret,
308 instrumentation,
309 accessor.InsSize(),
310 /* unlock_monitors= */ false);
311 return ret;
312 }
313 if (UNLIKELY(self->IsExceptionPending())) {
314 instrumentation->MethodUnwindEvent(self,
315 method,
316 0);
317 JValue ret = JValue();
318 if (UNLIKELY(shadow_frame.GetForcePopFrame())) {
319 DCHECK(Runtime::Current()->AreNonStandardExitsEnabled());
320 PerformNonStandardReturn(self,
321 shadow_frame,
322 ret,
323 instrumentation,
324 accessor.InsSize(),
325 /* unlock_monitors= */ false);
326 }
327 return ret;
328 }
329 }
330 }
331
332 ArtMethod* method = shadow_frame.GetMethod();
333
334 DCheckStaticState(self, method);
335
336 // Lock counting is a special version of accessibility checks, and for simplicity and
337 // reduction of template parameters, we gate it behind access-checks mode.
338 DCHECK_IMPLIES(method->SkipAccessChecks(), !method->MustCountLocks());
339
340 VLOG(interpreter) << "Interpreting " << method->PrettyMethod();
341
342 return ExecuteSwitch(
343 self, accessor, shadow_frame, result_register, /*interpret_one_instruction=*/ false);
344 }
这段代码是 Android ART (Android Runtime) 的一部分,用于执行 Java 方法。这个函数体的核心功能是通过解释执行或 JIT 编译执行方法,同时处理相关的事件和异常。273行可以插入打印函数名的方法以便用于打印当前执行的 Java 方法的名称。
以下是对该代码段的详细解释:
-
函数定义和参数:
-
Thread* self
: 当前线程的引用。 -
const CodeItemDataAccessor& accessor
: 用于访问方法的代码项,包括局部变量和指令。 -
ShadowFrame& shadow_frame
: 代表当前方法的栈帧,用于管理局部变量和方法执行状态。 -
JValue result_register
: 用于存储方法返回值的变量。 -
bool stay_in_interpreter
: 指示是否应保持在解释器中执行,即使存在 JIT 编译的代码。 -
bool from_deoptimize
: 表示此方法调用是从优化代码中退化回来的。 -
检查方法属性:
这里使用 DCHECK 确保当前方法不是抽象的也不是本地的,因为这些类型的方法不能直接通过解释器执行。 -
缓存事件通知需求:
设置是否需要在 Dex PC(程序计数器)改变时通知,这是性能优化的一部分,避免频繁检查。
-
JIT 编译执行:
如果环境允许,尝试使用 JIT 编译的代码执行方法,而不是继续在解释器中执行。 如果有 JIT 编译的代码,那么执行 JIT 编译的代码,并在执行前后进行适当的栈帧管理。 -
方法进入和退出事件:
如果存在方法进入事件的监听器,或者需要强制弹出栈帧,则触发相应的事件。 -
异常处理和非标准退出:
如果在执行过程中发生异常或需要进行非标准退出(例如由于强制弹出栈帧),则处理这些情况并返回相应的结果。 -
继续解释执行:
如果没有使用 JIT 编译的代码执行,则继续使用解释器执行方法。
ExecuteSwitch是执行解释操作的函数,它负责按照字节码指令逐一执行。
4 oat和art文件
oat文件本质上是一个ELF文
件,它将OAT文件格式内嵌在ELF文件里
可以将art文件看作是很多对象通过类似序列化的方法保存到文件里而得来的。当art文件通过mmap加载到内存时,这些文件里的信息就能转换成对象直接使用。
如果在dex2oat时不生成art文件的话,那么上述这些对象只能等到程序运行时才创建,如此将耗费定的运行时间。考虑到boot包含的内容非常多(13个jar包,14个dex文件),所以在Android 7.0中,boot镜像必须生成art文件。而对app来说,默认只生成oat文件。其art文件会根据profile的情况由系统的后台服务择机生成。这样能减少安装的时间,提升用户体验。