究竟是什么导致了';自旋暂停';Android中的错误?

究竟是什么导致了';自旋暂停';Android中的错误?,android,java-native-interface,dalvik,Android,Java Native Interface,Dalvik,我目前在调试一些依赖于本机库的Android代码时遇到问题。一个本机调用似乎特别容易出现这种“挂起时旋转”错误。它通常表现为: threadid=2: spin on suspend #2 threadid=48 (pcf=3) 到目前为止,我还不能确定到底是什么失败了,除了在大约10条消息之后,我的应用程序遇到了一个SIGSTKFLT并退出。每次,第一个线程是GC,第二个线程是当前正在执行本机代码的任何线程。与此消息一起打印的堆栈部分在堆栈顶部始终有一个本机方法 当Dalvik对此抱怨时,到

我目前在调试一些依赖于本机库的Android代码时遇到问题。一个本机调用似乎特别容易出现这种“挂起时旋转”错误。它通常表现为:

threadid=2: spin on suspend #2 threadid=48 (pcf=3)
到目前为止,我还不能确定到底是什么失败了,除了在大约10条消息之后,我的应用程序遇到了一个
SIGSTKFLT
并退出。每次,第一个线程是GC,第二个线程是当前正在执行本机代码的任何线程。与此消息一起打印的堆栈部分在堆栈顶部始终有一个本机方法

当Dalvik对此抱怨时,到底发生了什么,我如何开始调试原因以便修复它

编辑:一个有趣的问题——在本地开发人员进行了一些更改之后,我现在有时也会看到以下错误:

PopFrame missed the break
VM aborting
Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1)

对我来说,线程转储在堆栈顶部显示我的本机方法也是非常奇怪的,然而线程状态是
可运行的
,而不是
本机的
——这怎么可能呢?

基本问题是Dalvik是一个安全点挂起VM,并使用“停止世界”垃圾收集。这意味着,为了让GC运行,它必须等待所有线程到达一个可以确保它们不会改变堆的点

由于某些原因,您的一个线程没有响应GC线程的挂起请求。它实际上不是在本机代码中执行的;如果是,线程将处于
NATIVE
状态,这被认为是安全的。(对本机堆的所有访问都通过JNI调用进行选通,所有JNI调用都执行挂起检查。)

出于性能原因,JIT能够以跳过挂起检查的方式将编译代码块链接在一起。如果一个线程挂起的时间太长,挂起的线程将“取消”块,并等待更长的时间。最终它开始抱怨,最终它放弃并中止VM

有些设备使用的是错误的,在紧密循环中可能会发生中止。在这种情况下,我不希望在堆栈顶部看到本机方法

调试的最佳选择是在gdb不满意时连接它,并尝试找出目标线程正在做什么。本机代码可能以某种方式破坏了VM状态或返回堆栈,因此从本机代码返回时,线程会被阻塞

编辑后更新:使用
dvmPopFrame()。当VM调用您的本机方法时,它会插入一个“中断”帧,以便在展开堆栈以进行异常处理时,VM不会越过调用站点。(它还用于VM发出的托管代码方法调用,例如用于反射或
)消息
PopFrame未找到中断
表示未找到中断帧

中断帧具有空方法指针。展开堆栈时,只要它看到一个非空的方法指针(表示它不是中断帧)和一个非空的前一帧指针(表示您没有到达堆栈顶部),它就会继续。如果您到达了堆栈的顶部,您就错过了中断——所有Dalvik堆栈都从一个真正的方法开始(如果线程通过JNI连接到VM,则有时是一个“假”方法)

因此,我猜测本机代码会破坏堆栈,使前一帧指针为空。解决这个问题的一种技术是让VM调用一个调用实际本机方法的本机方法;“中间人”在堆栈上分配一些内容,将其设置为已知值,调用实际方法,然后在返回之前验证其堆栈分配是否保持不变

(可能需要使用这些值,以防止编译器优化它们;如果您使用以下内容:

if (jniEnv == NULL) {
    printf("my stuff is ...", ...);
}
然后它将永远不会实际运行,因为
JNIEnv*
永远不会为null……但编译器不知道这一点。)

有关Dalvik堆栈布局的完整说明,请参阅


从本机代码返回时,线程处于
RUNNABLE
状态是正常的。您的本机方法仍然处于顶部,因为弹出它的代码失败并中止了VM。

那么,如果我的本机代码破坏了VM状态,为什么它不一致地失败?有时,调用返回的结果很好(可能是因为GC在该点上没有发生)。我不应该得到一个segfault或类似的吗?任何事情都有可能。第一步是弄清楚“卡住”的线程实际上在做什么。你会在线程转储中看到一个“sysTid”值——这是Linux线程ID,你也会在gdb中看到它。我想我已经掌握了堆栈是如何被破坏的——我注意到在JNI代码中,开发人员将JNIEnv*指针隐藏在一个静态字段中,然后在另一个本机方法调用中重用它(发生在不同的线程上)。我之所以发现这一点,是因为当第二个JNI方法调用回Java时,我有一个线程“复活了”。如果您启用CheckJNI功能,当发生这种情况时,您将收到强烈的警报。看,我刚刚投票赞成这个。我想我从来没有见过关于一个我应该知道的话题的如此全面的答案,我显然没有。法登有一些很酷的经历!