背景
在Android ART虚拟机中,GC的部分流程中会执行stop the world的操作,那么,STW在虚拟机中如何实现呢?本文就深入到ART虚拟机源码中,探寻STW的实现过程。
【本文基于android12源码分析】
CheckPoint机制
ART虚拟机中采用的是主动式挂起暂停某个线程的执行。也就是说,当GC或者其他操作需要挂起线程的时候,不直接对线程进行操作,而仅仅是简单地给线程设置一个标志,线程执行过程中主动去轮询这个标志,发现这个标志是挂起状态时,主动暂停本线程的执行。轮训检查这个标志的位置就叫做检查点(Check Point)。在JVM虚拟机(HotSpot VM)中,叫做安全点(Safe Point)。
虚拟机的Check Point机制主要包括三个部分内容:
- 有一个标志位控制变量,外部可以设置它;
- 无论是解释执行还是机器码执行过程中,需要经常去检查这个标志位是否有变化,检查标志位的地方就是Check Point;
- 如果检查到标志位有变化,则执行其他操作,这个操作包括:暂停线程执行,遍历堆栈,调用垃圾回收等等;
CheckPoint标志位和线程状态
每个Java线程都需要有一个存储线程是否需要挂起的标志位变量,由于Java线程在ART虚拟机中都对应了一个art::Thread指针,因此,这个变量就保存在art::Thread类的state_and_flags
成员变量中。
这个成员变量的值由两个枚举类ThreadFlag
和ThreadState
组成。
ThreadFlag
枚举类中定义了CheckPoint相关的线程状态。
先看标志位的枚举类型:
art/runtime/thread.h1
2
3
4
5
6
7enum ThreadFlag {
kSuspendRequest = 1, // If set implies that suspend_count_ > 0 and the Thread should enter the
// safepoint handler.
kCheckpointRequest = 2, // Request that the thread do some checkpoint work and then continue.
kEmptyCheckpointRequest = 4, // Request that the thread do empty checkpoint and then continue.
kActiveSuspendBarrier = 8, // Register that at least 1 suspend barrier needs to be passed.
};
ThreadFlag中的几个枚举值定义了虚拟机使用check point机制的四种场景:
- kSuspendRequest:如果设置该标志,表示着suspend_count大于0,则要求线程进入到挂起状态;
- kCheckpointRequest:如果设置该标志,则请求当前线程执行check point任务,执行完成后继续原任务;
- kEmptyCheckpointRequest:如果设置该标志,则请求当前线程执行空的check point任务,执行完成后继续原任务;
- kActiveSuspendBarrier:如果设置该标志,则要求至少设置一个挂起栅栏给当前线程,这个标志只在设置了kSuspendRequest标志才生效,并且仅用于挂起所有线程(SuspendAll)的流程中。这个机制类似于Java多线程并发编程中的栅栏(CyclicBarrier)。
ThreadState
枚举类中定义了虚拟机中线程所有可能的状态:
1 |
|
checkPoint相关的flag和线程状态相关的state,保存在art::Thread类StateAndFlags
联合体中。
StateAndFlags
联合体的定义如下:
1 | class Thread{ |
数据结构StateAndFlags
是一个联合体,长度为32位,包含三个身份:
- as_struct:包含两个长度为16位的成员,低16位为flags,这个就是跟Check Point机制相关的状态变量,取值来自于枚举类ThreadFlag,高16位为state,表示线程运行状态,取值来自于枚举类ThreadState;
- as_atomic_int: 用于整体设置as_struct的值,由于是原子操作,确保了线程安全;
- as_int: 用于整体设置as_struct的值,非线程安全;
线程挂起恢复流程
挂起函数
虚拟机中,挂起所有线程和挂起单个线程,分别调用了thread_list.cc文件中的SuspendAll()
函数和SuspendThreadByThreadId()
函数。这两个函数的核心流程都是调用了thread.cc中的ModifySuspendCountInternal
函数,这个函数的实现如下:
1 | bool Thread::ModifySuspendCountInternal(Thread* self, |
ModifySuspendCountInternal
函数的逻辑很好理解,要点有:
- 如果需要暂停线程,传入的参数delta设置为1,那么将会给线程的成员变量
state_and_flags
设置kSuspendRequest
这个flag,标识这个线程需要被挂起; - 如果需要恢复线程,传入的参数delta设置为-1,如果
tls32_.suspend_count + delta
的值变成了0,则清除线程的kSuspendRequest
标志位。
这个函数的另外一个参数suspend_barrier
是用于挂起所有线程的,将suspend_barrier
地址保存到每个需要挂起的线程中,并给线程设置kActiveSuspendBarrier
标志位。每个线程挂起完成后,都会将suspend_barrier
减去1,只要等到suspend_barrier
的值为0,就可以知道所有线程都完成挂起。后面的SuspendAll
流程中再详细分析这个过程。
另外, TriggerSupend()
是 Android 5.1 版本之前旧的实现,通过注释可以看到,这是隐式的触发suspend的实现,主要原理是通过一个空指针异常触发一个SIGSEGV信号,然后在 signal_handler中,切换到suspend check,进行suspend的。不过现在 ART 源码里已经找不到读取tlsPtr_.suspend_trigger
的代码,这个逻辑已经被废弃。
从其代码注释中也可以看出来:
1 | // Trigger a suspend check by making the suspend_trigger_ TLS value an invalid pointer. |
CheckSuspend
函数
在Android源码中搜索kSuspendRequest
使用之处,仅发现三处,分别是函数:CheckSuspend
TransitionFromSuspendedToRunnable
TransitionFromSuspendedToRunnable
这里,CheckSuspend
函数是整个CheckPoint机制中最核心的一个函数。上文提到,无论是在解释执行或者机器码执行的流程中,都会注入检查点,这个检查点就是CheckSuspend
函数。
CheckSuspend
函数的实现如下:
1 | inline void Thread::CheckSuspend() { |
逻辑比较简单,判断当前线程的state_and_flag
是否设置kCheckpointRequest
,kSuspendRequest
,kEmptyCheckpointRequest
这三个标志位,如果有设置,则分别做相应的处理。
它们的作用分别为:
kCheckpointRequest
:调用函数RunCheckpointFunction()
,执行Check Point闭包函数;kSuspendRequest
:调用FullSuspendCheck()
,将线程挂起;kEmptyCheckpointRequest
:调用RunEmptyCheckpoint()
,执行空的Check Point闭包函数;
先看看FullSuspendCheck
函数中将线程挂起的实现过程。
线程挂起
如果线程设置了 kSuspendRequest
标志位,则执行FullSuspendCheck()
,这个函数的实现如下:
1 | void Thread::FullSuspendCheck() { |
这个函数只是构造一个栈上的ScopedThreadSuspensio
类的对象,这个对象的作用是:
- 在构造函数中调用了
TransitionFromRunnableToSuspended
函数,将线程状态(state_and_flag
中的state)从kRunnable
切换到kSuspended
- 在析构函数中调用了
TransitionFromSuspendedToRunnable
函数,,将线程状态从kSuspended
切换到kRunnable
,这个过程可能会使线程进入等待状态,从而实现线程的挂起。
ScopedThreadSuspension
类的构造函数和析构函数实现如下:
1 | inline ScopedThreadSuspension::ScopedThreadSuspension(Thread* self, ThreadState suspended_state) |
TransitionFromRunnableToSuspended 函数
TransitionFromRunnableToSuspended
函数的实现如下:
这个函数主要调用了TransitionToSuspendedAndRunCheckpoints
和mutator_lock的TransitionFromRunnableToSuspended()函数,其中前者实现线程状态到kSuspended的切换,后者是释放当前线程对mutator_lock_的 shared hold,Runnable状态的线程都会持有mutator_lock_,切换到Suspended状态时需要释放掉。
TransitionToSuspendedAndRunCheckpoints
函数时实现如下:
这个函数最核心的内容就是通过CAS的方式将线程的state_and_flags
成员变量设置 kSuspended
的state,从而完成线程状态从kRunnable
到kSuspended
的切换。
在切换线程状态前,先判断是否设置了kCheckpointRequest
标志位,如果有设置,则需要先执行Check Point闭包函数。这应该是为了避免线程挂起后,已设置的Check Point函数无法得到及时执行。
TransitionFromSuspendedToRunnable 函数
接下来,在ScopedThreadSuspension
类的析构函数中调用了TransitionFromSuspendedToRunnable
函数,最终实现线程的挂起。
源码如下:
根据函数名可知,这个函数会将线程状态从kSuspended
切换为kRunnable
。这个流程里会判断state_and_flags
里的flag是否设置了kSuspendRequest
标志位,如果设置,则通过调用 Thread::resume_cond_->Wait(thread_to_pass)
函数使线程进入等待状态,完成线程的挂起操作。
挂起结束恢复线程时,在while循环退出前调用了 Locks::mutator_lock_->TransitionFromSuspendedToRunnable(this)
,通过shared方式持有全局锁mutator_lock_,使得Runnable状态的线程持有了这个锁,线程继续执行。
TransitionFromSuspendedToRunnable
函数看起来很复杂,其实里面很多内容只不过是因为用了无锁编程而引入的”套路“代码。
线程恢复
线程恢复的流程比较简单。
恢复线程需要调用thread_list.cc中的Resume()
函数,代码如下:
Resume函数中,先调用ModifySuspendCount
将线程的suspend_count减1,然后再ModifySuspendCountInternal
函数中,当线程的suspend_count等于0时,会清除线程state_and_flags
的kSuspendRequest
的标志位,清除挂起标志位后,调用Thread::resume_cond_->Broadcast()
唤醒线程。
线程将从TransitionFromSuspendedToRunnable
函数的Thread::resume_cond_->Wait()
处被唤醒,开始继续原来的执行流程,并且线程的状态切换为kRunnable
。
Wait 和 Broadcast 的实现原理
以上分析了线程挂起和恢复的流程,其中,
线程挂起调用的是Wait
函数:
1 | Thread::resume_cond_->Wait(thread_to_pass) |
线程恢复调用的是Broadcast
函数:
1 | Thread::resume_cond_->Broadcast(self) |
resume_cond_
是类art::Thread
中的静态成员变量,对应的类型是ConditionVariable
:
1 | class Thread { |
类ConditionVariable
中Wait()
函数的实现为:
根据宏ART_USE_FUTUXES
来判断是使用futex
系统调用挂起线程还是使用pthread_cond_wait
来挂起。
显然,这里使用了futex
,因为宏ART_USE_FUTUXES
的定义如下:
另外,在Android的bionic库中,pthread_cond_wait
最终实现其实也是futex
系统调用:
再看看线程恢复函数Broadcast
的实现:
[art/runtime/base/mutex.cc]
最终也是调用的futex
函数实现的,跟挂起时相比,只是传入的flag不相同而已。
线程挂起时,调用futex传入flag:
FUTEX_WAIT_PRIVATE
,实现线程等待;线程恢复时,调用futex传入flag:
FUTEX_REQUEUE_PRIVATE
,实现线程唤醒;
这两个宏定义在futex.h头文件中:
1 | #define FUTEX_WAIT 0 // 等待 |
Futex是linux上一种用户态和内核态混合的同步机制,支持进程内的线程之间和进程间的同步锁操作。
futex原理可参考文档:linux–futex原理分析
至此,完成的线程挂起和恢复的流程分析。
SuspendAll和ResumeAll
SuspendAll
用于暂停所有Java线程的执行,而ResumeAll
用于恢复所有Java线程的执行。
下面分别介绍这两个函数的实现过程。
SuspendAll流程
SuspendAll
函数最终调用到了SuspendAllInternal
函数,这个函数的具体实现如下:
1 | void ThreadList::SuspendAllInternal(Thread* self, |
这个函数的核心逻辑包含三部分,分别是:
- 将需要挂起的线程数量设置到
pending_threads
变量中,此变量起到栅栏作用,用于等待所有线程的挂起完成; - 遍历所有线程,调用针对每个线程调用
ModifySuspendCount
函数,传入pending_threads
的地址,将线程的suspend_cout
加1,设置kSuspendRequest标志,并且设置kActiveSuspendBarrier标志(表示此次挂起需要等待栅栏); - 等待所有线程挂起完成,方法是在循环语句中检测
pending_threads
值是否为0,不为0则通过futex系统调用继续等待,直到等于0,则所有线程挂起成功,完成SuspendAll的流程;
SuspendAllInternal函数跟Suspend单个线程的核心不同点是SuspendBarrier
的栅栏处理流程。
挂起多个线程,需要使用栅栏等待所有线程完成挂起,挂起完成后通知到调用侧。
每个线程执行到Check Point后完成本线程的挂起,同时也要处理栅栏。此时,会调用art::Thread
的PassActiveSuspendBarriers
函数。PassActiveSuspendBarriers
源码如下:
1 | bool Thread::PassActiveSuspendBarriers(Thread* self) { |
这个函数其实很简单,主要包含两个逻辑:
- 将调用SuspendAllInternal时传入本线程的计数器
pending_threads
指针变量的值减1; - 当
pending_threads
减小到0时,调用futex FUTEX_WAKE_PRIVATE
,唤醒调用SuspendAllInternal的线程,通知它所有线程已完成挂起;
ResumeAll流程
ResumeAll用于恢复所有暂停线程,使其继续执行。
由于这里并不用等待暂停线程的状态变更,所以实现非常简单,跟恢复单个线程执行的流程基本一致。
核心逻辑就两条:
- thread->ModifySuspendCount:将所有线程的
suspend_count
减1,如果suspend_count
等于0,则清除线程state_and_flags
的kSuspendRequest
的标志; - Thread::resume_cond_->Broadcast:通知所有线程,如果线程的
state_and_flags
的kSuspendRequest
的标志被清除就立即恢复执行;
源码如下:
art/runtime/thread_list.cc
1 | void ThreadList::ResumeAll() { |
执行CheckPoint闭包任务
在上面CheckSuspend()
函数的分析中,如果线程的state_and_flag变量设置了kCheckpointRequest
标志位,则会调用RunCheckpointFunction()
函数,执行检查点任务。
下面详细分析此流程。
执行Closure任务流程
RunCheckpointFunction
函数的代码如下:art/runtime/thread.cc
1 | Closure* checkpoint_function GUARDED_BY(Locks::thread_suspend_count_lock_); |
这个函数的实现并不复杂,主要包含两点:
- 保存线程成员变量
checkpoint_function
到checkpoint指针中,并执行这个Closure任务; - 将线程成员变量
checkpoint_overflow_
这个list中的头部元素取出保存到checkpoint_function
中,下次继续执行这个Closure任务,如果list中任务已经消费完,则清除线程的kCheckpointRequest
标志;
其中,
线程的checkpoint_function
变量:保存了当前需要执行的Closure任务
线程的list变量checkpoint_overflow_
:保存了下次需要执行的所有Closure任务。
这里,Closure
是一个非常简单的纯虚类,内部仅包含一个析构函数和一个虚函数Run()
,有点类似于Java的Runnable
接口:
1 | class Closure { |
设置Closure任务
下面看看如何设置Closure任务。
thread.cc中有两个可设置Closure任务的函数:RequestSynchronousCheckpoint
和RequestCheckpoint
。
前者是同步的,也就是设置后,当前线程会等待目标线程Closure任务执行完成。
后者是异步的,只给线程设置Closure任务,不关心执行完成的时间。
这里介绍下RequestCheckpoint
的实现。
其源码如下:art/runtime/thread.cc
1 | bool Thread::RequestCheckpoint(Closure* function) { |
这个函数的要点有:
- 线程的状态如果非
kRunnable
,则无法设置Closure任务,返回false; - 给线程设置
kCheckpointRequest
的标志位; - 如果线程的
checkpoint_function
为空,则将任务保存到里面,如果非空,则保存到这个list中checkpoint_overflow_
;
所有线程执行Closure任务
在thread_list.cc中,还有一个函数可以设置所有线程执行某个Closure任务:RunCheckpoint
。
RunCheckpoint
函数的要点有:
- 遍历所有Java线程,针对每个线程调用
RequestCheckpoint
函数,设置线程的Closure任务; - 如果线程是非
kRunnable
状态,RequestCheckpoint
会设置任务失败,此时会调用ModifySuspendCount
函数尝试挂起线程,并轮训等待线程挂起完成,挂起成功后将线程指针保存到suspended_count_modified_threads
这个vector中; - 遍历保存了挂起线程的vector:
suspended_count_modified_threads
,在本线程执行Closure任务,然后恢复线程的执行。
代码如下:
1 | size_t ThreadList::RunCheckpoint(Closure* checkpoint_function, Closure* callback) { |
通过RunCheckpoint
可知,它会要求所有线程都执行指定的Closure任务,假如某些线程非Runnable状态(比如kSuspend状态),无法执行Closure任务,则由调用者来代替这些线程执行Closure任务,即是在调用者线程里直接调用Run函数,并传入真正的目标线程对象。
哪些情况下会用到这种强操作呢?
比如,应用程序的某些重要线程阻塞了,但我们又需要打印所有线程的调用栈信息以协助排查问题。显然,让阻塞的线程从阻塞处退出并进入CheckPoint点以执行打印调用栈是不现实的。此时,RunCheckpoint
的这种处理方式就派上用场了。
源码中Check Point闭包使用案例
打印线程调用堆栈:DumpCheckPoint
当App发生ANR时,会发送一个SIGQUIT信号,在信号处理器SignalCatcher中,监听到SIGQUIT后,调用了signal_catcher
的HandleSigQuit()
函数,代码如下:
在HandleSigQuit
函数中,调用了runtime的DumpForSigQuit
函数
在Runtime的DumpForSigQuit
函数中,调用了thread_list.cc中的DumpForSigQuit
函数,打印所有线程的调用堆栈,包括Java线程和native线程:
DumpForSigQuit
函数最终调用了thread_list.cc中的Dump
函数打印线程堆栈,Dump函数中,构造了一个DumpCheckPoint
类型的闭包任务,然后调用RunCheckPoint
函数,通知所有Java线程执行这个闭包任务(挂起的线程在当前线程中执行闭包),代码如下:
上面已经介绍RunCheckPoint
函数的执行流程,这里重点介绍下thread_list.cc中的DumpCheckPoint
这个类的具体实现。
DumpCheckpoint
继承了纯虚类Closure
,实现了虚函数Run
函数,在Run函数中调用Thread的Dump
函数输出当前线程的调用堆栈。
DumpCheckpoint
类中,比较难理解的点是成员变量barrier_
以及函数WaitForThreadsToRunThroughCheckpoint
。这里重点介绍下。
Barrier
这个词的本意是栅栏,在ART虚拟机中,它是当做多线程同步计数器来使用。比如,有6个线程,其中一个线程需要等待其他5个线程执行完某个任务才能返回,这种情形下,就可以使用Barrier
来实现。
具体用法如下:
- 创建一个
Barrier
对象,设置初始值为5,并将这个对象传递给另外5个线程; - 这5个线程执行完任务后,调用
Barrier
的Pass
函数,该函数会将Barrier
的计数器减1; - 第6个线程将等待着这个
Barrier
对象,直到其计数器变成0;
在thread_list.cc的Dump
函数中,调用了RunCheckPoint
,根据上面的分析,这个函数会返回需要执行check point任务的线程数量,然后将这个数量传给WaitForThreadsToRunThroughCheckpoint
函数,将barrier_
的计数器加上线程的数量,并等待barrier_
计数器减少到0。
而在DumpCheckpoint
的Run
函数中,执行完后会将barrier_的计数器减去1,变成0后,说明所有线程的DumpCheckpoint
都执行完,WaitForThreadsToRunThroughCheckpoint
函数从barrier_.Increment
处开始恢复执行,代码如下:
1 | // A closure used by Thread::Dump. |
这里使用Barrier
的原因是,在RunCheckPoint
函数中,如果线程是kRunnable
状态,则会将check point任务丢到该线程的check point点处,在本线程中执行。因此,需要Dump线程等待其他线程执行完check point任务。
Jit垃圾回收:MarkCodeClosure
在JIT流程中,有一个类JitCodeCache,它提供了一个存储空间,用于存放JIT编译的结果。当编译结果超过一定的阈值时(大约是64M),该部分空间会被释放,也就是JIT的GC过程。
对于JIT的GC而言,使用的算是Mark-Sweep,即标记清除法。
标记时,需要判断所有线程的正在执行的方法栈是否有使用到JIT编译的机器码,如果有,则标记这些方法对应的code cache,清除时忽略这些方法对应的机器码。
JIT GC的标记对应的函数是jit_code_cache.cc中的 MarkCompiledCodeOnThreadStacks
,这个函数具体实现如下:
1 | void JitCodeCache::MarkCompiledCodeOnThreadStacks(Thread* self) { |
这里的代码逻辑跟ThreadList::Dump()
中使用DumpCheckPoint
的流程基本一致,先是构造一个Check point的Closure类,然后调用ThreadList的RunCheckpoint
函数让所有线程执行这个Closure,最后,通过barrier等待每个线程的Closure执行完成。
在MarkCodeClosure
的Run
函数中,通过StackVisitor
遍历线程的当前调用栈,如果有quick_code,则判断quick_code是否在jit_code_cache中,如果在,则在CodeCacheBitmap中标记当前quick_code的地址。在JIT的GC的清除阶段,就不清除对应的机器码。
代码如下:
1 | class MarkCodeClosure final : public Closure { |
检查点(CheckPPoint)的插入流程
上面分析了挂起函数的实现,当主动触发线程挂起时,并没有直接让线程暂停执行,而仅仅是将线程的state_and_flags
设置了kSuspendRequest
标志位,最终是在Thread::CheckSuspend()
函数中执行的线程挂起操作。
那么Thread::CheckSuspend()
函数又是在何时何处被调用的呢?
答案是,在Check Point(检查点)中被执行的。在Java代码正常执行的过程中,会插入一些检查点,当代码执行到检查点时,会挂起当前线程,而执行其他任务,这些其他任务包括:垃圾回收、Debug、获取调用栈等等。
在ART虚拟机中,检查点存在下面几个位置(解释执行和机器码执行略有区别):
- 方法结束时
- 条件语句分支
- for\while 循环一次结束的出口
- 抛出异常的地方
下面分别分析解释执行和机器码执行流程中,检查点是如何被插入的。
解释执行
先看看解释执行中的检查点。
[art/runtime/interpreter/interpreter_switch_impl-inl.h]
异常处理流程中的检查点:
方法返回流程中的检查点:
Goto, switch等跳转中的检查点:
其中 Self()->AllowThreadSuspension() 就是挂起线程的检查点,它最终会执行到 Thread::CheckSuspend()
中,这里的 Self() 就是当前的Thread指针,代码如下:
1 | inline void Thread::AllowThreadSuspension() { |
可以看到,最终调到了CheckSuspend
函数。
机器码执行
字节指令的机器码是在dex2oat或者jit流程中编译生成的,在编译器生成quick code时,会在生成代码的各个流程中插入调用CheckSuspend
函数的指令。
生成机器码流程
这里以arm64位指令生成过程为例,介绍机器码生成过程中,是如何插入CheckSuspend
函数的相关指令。
机器码中的检查点跟解释执行模式稍有不同,主要安装在以下位置:
- 函数入口设置一个检查点;
- 循环头(Loop Handler)设置一个检查点;
- HGoTo IR处理中,如果存在往回跳转的情况,也会设置一个检查点;
具体来说,构造IR时,在HInstructionBuilder
的Build
函数中,在以下两处添加SuspendCheck的IR:
- Entry Block中设置一个检查点,其中
HSuspendCheck
是用于生成检查点机器码对应的IR; - Loop Header Block中设置一个
HSuspendCheck
;
代码如下:
art/compiler/optimizing/instruction_builder.cc
1 | bool HInstructionBuilder::Build() { |
最终生成机器码时,在CodeGenerator中,会针对HSuspendCheck
和HGoTo
IR生成与检查点相关的机器码,代码如下:
art/compiler/optimizing/code_generator_arm64.cc
1 | void InstructionCodeGeneratorARM64::VisitSuspendCheck(HSuspendCheck* instruction) { |
在HGoTo
IR中,也有HSuspendCheck
的处理流程,这里针对循环回跳和Entry Block分别设置了检查点:
art/compiler/optimizing/code_generator_arm64.cc
1 | void InstructionCodeGeneratorARM64::VisitGoto(HGoto* got) { |
再看看GenerateSuspendCheck()
函数中生成SuspendCheck函数调用对应的指令的步骤,主要包括三个步骤:
- 创建一个
SuspendCheckSlowPathARM64
对象; - 根据线程的
state_and_flag
成员变量的偏移,并生成ldhr指令; - 生成cbnz或者cnd指令;
art/compiler/optimizing/code_generator_arm64.cc
1 | void InstructionCodeGeneratorARM64::GenerateSuspendCheck(HSuspendCheck* instruction, |
再看看SuspendCheckSlowPathARM64
这个类,检查点对应的机器码在这个类的EmitNativeCode
函数生成:
1 | class SuspendCheckSlowPathARM64 : public SlowPathCodeARM64 { |
kQuickTestSuspend
定义在QuickEntrypointEnum
这个枚举类中,这里使用了两层#define宏定义包裹:
art/runtime/entrypoints/quick/quick_entrypoints_enum.h
1 | // Define an enum for the entrypoints. Names are prepended a 'kQuick'. |
kQuickTestSuspend
对应的函数的声明在QuickEntryPoints
结构体中,对应的函数名称是pTestSuspend
,这里的QuickEntryPoints
结构体,在thread.h中也被使用到:
1 | class Thread { |
art/runtime/entrypoints/quick/quick_entrypoints.h
1 | // Pointers to functions that are called by quick compiler generated code via thread-local storage. |
以上两处,共同使用QUICK_ENTRYPOINT_LIST
的宏定义:
art/runtime/entrypoints/quick/quick_entrypoints_list.h
1 | // All quick entrypoints. Format is name, return type, argument types. |
pTestSuspend
函数在线程初始化时设置为art_quick_test_suspend
函数的地址,进而调用到SuspendCheck
函数,下文具体分析这个流程。
生成机器码结果
这里以一个非常简单的java方法为例,使用下面的命令编译出release apk的odex文件:
adb shell cmd package compile -m speed -f my-package
分析其编译后的机器码中的指令(Android10):
方法开头的一些指令是进行栈溢出检测,开辟栈空间以及保存寄存的操作。
看红框中的指令,读取了x19寄存器(tr)中的值到w16寄存器中,x19寄存器中保存的是当前线程的地址,thread指针偏移为0出的成员变量刚好就是thread的state_and_flags
,从源码中可以看出:
红框中这两条指令:
0x0059232c: 79400270 ldrh w16, [tr] ; state_and_flags
0x00592330: 35000670 cbnz w16, #+0xcc (addr 0x5923fc)
意思就是,当前线程的state_and_flags
的值不为0,就跳转到地址0x5923fc执行。
地址0x5923fc处的指令如下:
红框中的指令,读取x19寄存器中地址偏移1352处的内容到lr寄存器中,然后跳转到这个地址中执行。
x19寄存器保存的是当前线程的地址,当前线程偏移1352处刚好就是当前线程pTestSuspend
成员变量。(PS: 在Android12及以上版本编译后的机器码中,此处已改成直接跳转到目标函数。)
线程的pTestSuspend
成员变量的赋值是在线程初始化的流程中进行的:
在线程的初始化流程中的注册了EntryPoints,pTestSuspend
这个成员变量设置了函数art_quick_test_suspend
的地址,这是一个纯汇编实现的函数,arm64中实现如下:
[art/runtime/arch/arm64/quick_entrypoints_arm64.S]
直接跳转到了函数artTestSuspendFromCode
中:
[art/runtime/entrypoints/quick/quick_thread_entrypoints.cc]
最终也是执行了线程的CheckSuspend()
函数。
另外,在jni函数的调用结束时,也加入了检查点。
普通的jni方法调用时,会将线程状态切换到kNative,执行完成切换回kRunnable,因此这个流程不需要检查点。
但FastNative注解的jni方法,由于不会切换线程状态,因此,方法执行完成,会调用一次CheckSuspend()
。
至此,总结完解释执行和机器码执行中的checkPoint点的设置流程。
线程CheckPoint机制流程图
线程CheckPoint机制的整体流程图如下:
总结
ART虚拟机的Check Point机制主要为了实现两个功能:
- 线程运行时执行Check Point闭包任务;
- 挂起和恢复线程的执行;
虚拟机为了实现这两个功能,在Java线程对应的Native类中添加了state_and_flag
变量,通过设置kSuspendRequest
或者kCheckpointRequest
来通知线程需要执行挂起任务还是闭包任务。具体执行任务的时机是在CheckSuspend
函数中,这函数(检查点)被插入到了解释执行和机器码执行的多个流程中,这样,Runnable状态的线程就能快速执行到检查点,完成线程挂起或者执行闭包任务。
参考
- Android Source Code
- 《深入理解Android Java虚拟机ART》邓凡平 著