具体实施方式
以下将参照附图对本发明的具体实施方式进行详细说明。在以下的说明中,术语“内核空间”和“用户空间”是针对操作系统的内核而言的。在本发明中,操作系统可以是诸如Unix、Linux以及Windows的各种操作系统。为了简单起见,在本发明中,仅以Linux作为操作系统的示例。但是本领域的技术人员应该明白,本发明的方法和设备同样适用于其它操作系统。
Linux的虚拟地址空间为0至4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间”。因为每个进程可以通过系统调度进入内核,因此,Linux内核由系统内的所有进程共享。内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。
图2是示出本发明的总体发明构思的示意图。在本发明中,在作为监视目标的Java进程中创建辅助线程,并且在操作系统的调度器中插入探测器。当该探测器检测到该Java进程中的线程被阻塞时,向所述辅助线程发送用户定义的信号,接收到该用户定义的信号的辅助线程到JVM的栈中取回此时的调用栈信息,从而可以定位到Java源代码中的精确位置。这样就实现了将本地层中的瓶颈准确地链接回Java源代码。
参照图3,本发明提供了一种用于检测并定位Java程序的瓶颈的方法。图3示出了根据本发明的一个实施例的方法流程300,包括如下步骤:
步骤310:创建辅助线程,并将其挂接到JVM。
步骤320:在操作系统内核中插入探测器。
步骤330:探测器监视Java线程并且在Java线程被阻塞时向辅助线程发送信号。
步骤340:辅助线程接收信号,从JVM中取回调用栈信息并利用该信息定位到Java源代码中的对应位置。
这里需要说明的是,Java程序在被运行时表现为用户空间中的进程。JVM对应于一个独立运行的Java程序,即对应于一个Java进程。当启动一个Java程序时,一个JVM实例就被启动起来了,任何一个拥有public static void main(String[]args)函数的类都可以作为Java程序运行的起点在JVM上运行。
下面详细描述本发明的方法流程300中的各个步骤。
步骤310:创建辅助线程,并将其挂接到JVM
在步骤310中,在对应于所述Java程序的Java进程中创建辅助线程,并将所述辅助线程挂接到在该Java进程中创建的Java虚拟机。
例如,可以通过Java虚拟机工具接口(JVMTI)提供的回调机制来创建辅助线程,并且通过Java本地接口(JNI)提供的方法将创建的辅助线程挂接到JVM上。JVMTI可以用来监控JVM的一些行为。JNI是为了扩展Java标准类库以使之支持依赖于平台的特性而提供的接口。JNI接口允许以较低级的语言实现代码的一部分,然后令Java应用程序来调用这些以较低级语言编写的函数。
具体地说,在JVM启动初始化完成的位置设置回调函数。例如,利用JVMTI,通过以下代码来启动响应虚拟机初始化事件的回调函数机制。
jvmtiEventCallbacks callbacks;//声明
memset(&callbacks,0,sizeof(callbacks));//初始化
callbacks.VMInit=&vmInit;//编写的回调函数的入口
jvmti->SetEventCallbacks(&callbacks,sizeof(callbacks));//完成设置
jvmti->SetEventNotificationMode(JVMTI_ENABLE,JVMTI_EVENT_VM_INIT,NULL);//启用对虚拟机初始化事件的通知
上述代码的功能是把程序员自己编写的回调函数vmInit()的地址赋值给jvmtiEventCallbacks类型的callbacks结构的变量VMInit,该变量表示在虚拟机初始化事件发生时调用的回调函数的入口。通过调用SetEventCallbacks()方法完成设置,并且通过调用SetEventNotificationMode()方法启动对虚拟机初始化事件的通知,完成了回调函数vmInit()的设置。这样,当虚拟机初始化时,回调函数vmInit()就会被执行。要注意的是,为了便于说明,在本文中,不对公知方法或函数的参数进行描述,例如只简单将其表示为function()。而对于用户自定义的函数,由于其参数可以由用户任意定义,因此也省略对这种函数的参数的定义和描述。本领域技术人员通过这种描述完全能够知道如何实现本发明的方法。
在回调函数vmInit()中,调用JVMTI的RunAgentThread()方法创建新的辅助线程。
这里,需要说明的是,在一个创建了JVM的进程中,并不是所有线程都能够直接使用JVM。为了区别于创建的辅助线程,Java进程中的对应于Java应用程序的线程在本文中被称为“Java应用线程”,而Java应用线程和辅助线程统称为Java线程。Java应用线程能够直接访问JVM,而辅助线程不能直接访问JVM。这就需要通过JNI接口提供的AttachCurrentThread()方法将当前的辅助线程挂接(attach)到JVM环境上。进行上述挂接的目的是为了使辅助线程能够实现对JVM中的栈的访问。为了使辅助线程能够对线程阻塞事件进行快速反应,需要将辅助线程设置为高的调度优先级。
在描述步骤320之前,需要对用户空间中的Java线程和内核空间中的对应线程(这里称为本地任务(native task))之间的关系进行描述。Java线程的调用栈位于用户空间中的JVM内,而本地任务的调用栈位于内核空间中。当一个Java进程通过系统调度进入内核时,其Java线程在内核中对应于一个本地任务,该本地任务被内核的调度器调度进入处理器而执行。
当一个Java进程存在多个Java应用线程时,这些Java应用线程分别对应于内核中的一个本地任务,并且在上述的步骤310中创建的辅助线程同样对应于内核中的一个本地任务。如图4所示。图4是示出了用户空间中的Java线程和内核空间中的本地任务之间的关系的示意图。在图4中,示例性地示出了三个Java应用线程以及创建的辅助线程。Java应用线程1至Java应用线程1分别对应于本地任务1至本地任务3,辅助线程对应于本地任务4。Java线程在用户空间中通过Java线程ID进行识别,但是本地任务在内核空间中通过本地任务ID进行识别。此外,在JVM中有每个Java线程的对应栈。当在内核空间中检测到一个本地任务(例如本地任务2)被阻塞时,需要知道在用户空间中与之对应的Java线程(例如Java应用线程2),从而能够访问该Java线程的在JVM中的调用栈。
为了实现上述目的,可以在每个Java线程启动时,通过回调函数建立该Java线程和操作系统内核中的对应于该Java线程的本地任务之间的映射关系。具体地说,与步骤310类似,在JVM启动时设置回调函数。例如,利用JVMTI,通过以下代码来启动响应线程启动事件的回调函数机制。
jvmtiEventCallbacks callbacks;//声明
memset(&callbacks,0,sizeof(callbacks));//初始化
callbacks.ThreadStart=&threadStart;//编写的回调函数的入口
jvmti->SetEventCallbacks(&callbacks,sizeof(callbacks));//完成设置
jvmti->SetEventNotificationMode(JVMTI_ENABLE,JVMTI_EVENT_THREAD_START,NULL);//启用对线程启动事件的通知
上述代码的功能是把程序员自己编写的回调函数threadStart()的地址赋值给jvmtiEventCallbacks类型的callbacks结构的变量ThreadStart,该变量表示在线程启动事件发生时调用的回调函数的入口。通过调用SetEventCallbacks()方法完成设置,并且通过调用SetEventNotificationMode()方法启动对线程启动事件的通知,完成了回调函数threadStart()的设置。这样,当Java线程启动时,回调函数threadStart()就会被执行。
在回调函数threadStart()中,首先调用操作系统内核提供的系统调用函数,例如gettid(),获得当前Java线程的在内核空间中对应的本地任务的ID,即本地任务ID。然后,再调用JNI提供的机制,获得当前线程在JVM中的ID,即Java线程ID。然后,将获得的本地任务ID和Java线程ID相关联地存储到如图4所示的映射数据库中。通过上述方式,每当有线程启动时,该线程都会调用回调函数threadStart()将其在用户空间的Java线程ID与其在内核空间的本地任务ID之间的映射关系存储起来。下面的表1示出了在图4的情况下建立的映射关系的可能的例子。
表1
本地任务ID |
Java线程ID |
图4中的对应线程 |
5893 |
1 |
应用线程1 |
5901 |
2 |
应用线程2 |
5925 |
3 |
应用线程3 |
6012 |
21 |
辅助线程 |
这里需要说明的是,在映射数据库中实际只存储表1中的“本地任务ID”和“Java线程ID”这两栏,最后一栏是为了参照图4进行说明从而更好地理解本发明而加入的。此外,需要说明的是,在Java程序是多线程程序时才需要如上所述建立映射数据库。也就是说,当Java程序只有以main()作为起点的一个主线程时,可以省略上述的建立映射数据库的步骤。为了更好地描述本发明,下面以多线程程序的情况(即,建立了映射数据库的情况)作为示例,继续对方法流程300的其余步骤进行说明。
步骤320:在操作系统内核中插入探测器
首先,说明何为探测器。操作系统提供了事件回调机制以用于系统调试和扩展。例如,在Linux系统中,就提供了Kprobe/Jprobe机制。该机制允许在内核的特定代码处插入用户自定义的函数,这种函数被称为“Prober(探测器)”。
可以通过多种手段在操作系统内核中插入探测器。例如,可以由辅助线程通过JNI接口调用以内核的编程语言编写的函数而直接在内核调度器中插入作为探测器的相应函数。但是,为了更快速更高效地实现上述目的,可以利用操作系统提供的动态加载模块的机制,这种机制的好处在于可以让核心保持很小的尺寸同时非常灵活。这种机制允许将用户编写的模块加载到内核中与内核一起工作。为了实现在操作系统内核中插入探测器,还可以采用以下方式:预先编写一个内核监视模块;将该内核监视模块加载到内核中工作;由辅助线程向该内核监视模块传递参数并控制该内核监视模块来插入探测器。通过这样做,与由辅助线程直接插入探测器的方式相比,简化了辅助线程的工作,并且利用内核级的模块来实现探测器的插入,实现了更快的速度,使本发明的性能开销更小。
具体地说,例如,在Linux系统中,执行insmod命令来显式加载内核模块。根据本发明的一个实施例的内核监视模块就是通过执行insmod命令被加载到内核中的。该内核监视模块在被加载到内核中后,除非执行rmmod命令,否则将一直在内核中工作。
图5是示出了步骤320的处理的一个示例的示意图。在该实施例中,探测器是被加载到操作系统内核中的用户定义的模块(即上述的内核监视模块)插入到操作系统的调度器中的。辅助线程在被创建后,向该已加载的内核监视模块登记作为监视目标的Java进程的内核ID以及对辅助线程对应的本地任务ID。然后,内核监视模块将根据所登记的进程ID和辅助线程ID编写的回调函数插入到调度器中。
具体地,例如在Linux系统中,通过下述代码完成探测器的插入:
jprobe.kp.symbol_name=_switch_to;
jprobe.entry=j_switch_to;
其中,第一行语句指定要插入的内核代码位置,第二行语句指定了用户自定义的回调函数j_switch_to。这样就实现了在_switch_to这个内核函数中插入我们自定义的回调函数j_switch_to。即,每当内核函数_switch_to被调用时,就会调用j_switch_to。本领域技术人员公知的是,_switch_to函数每次在发生任务上下文切换时被调用,也就是说,同样地,被插入的探测器j_switch_to每当发生任务上下文切换时执行操作。
步骤330:探测器监视Java线程并且在Java线程被阻塞时向辅
助线程发送信号
在步骤330中,探测器监视所述Java进程中的Java线程在操作系统内核中的状态并且响应于检测到Java线程被阻塞而向所述辅助线程发送信号。
由于探测器j_switch_to被插入在_switch_to函数中,所以它可以获得_switch_to的全部参数,从而可以知道从处理器中调出而触发了任务上下文切换事件的本地任务的状态以及该本地任务属于哪个进程。也就是说,我们可以在自定义函数j_switch_to中定义探测器的行为来实现步骤330的处理。
探测器在步骤320中例如从内核监视模块获得了两个参数:JAVA进程的内核ID(PID)以及对应于辅助线程的本地任务的ID(HTID)。这两个参数是由辅助线程向内核监视模块登记的。在探测器中实现如下的判断逻辑:如果在处理器执行任务上下文切换时从处理器调出的本地任务对应于所述Java进程中的Java线程并且该本地任务处于阻塞状态,则由所述探测器向所述辅助线程发送信号。即,在同时满足(1)调出的本地任务属于PID指示的进程以及(2)调出的本地任务处于阻塞状态这两个条件的情况下,才向HTID指示的线程发送信号。
需要说明的是,一个本地任务从处理器调出可能有多种原因,其可能因为处于阻塞状态,也可能是因为分配的时间片届满而被调出处理器。在这些情况下都会调用探测器。因为必须满足条件(2)才发送信号,所以仅仅因为分配的时间片届满而被调出处理器的本地任务并不会触发向辅助线程发送信号,从而大大减小了性能开销。
所述发送信号可以采用多种方式进行。在一个实施例中,例如在Linux系统中,可以采用系统函数send_signal向辅助线程发送预先指定的信号。辅助线程一直等待在该信号上,辅助线程在信号被发出后被唤醒并接收该信号。在另一个实施例中,可以在用户空间和内核空间之间建立通信通道,并且当同时满足上述的条件(1)和(2)时,探测器通过该通信通道与辅助线程通信以通知检测到阻塞。不论采用哪种方式,向辅助线程发送的信号包含被阻塞的本地任务的ID。
步骤340:辅助线程接收信号,从JVM中取回调用栈信息并利用
该信息定位到Java源代码中的对应位置
在步骤340中,辅助线程响应于接收到来自操作系统内核的信号从所述JVM中取回调用栈信息,并利用所取回的调用栈信息定位到所述Java程序的源代码中的对应位置。其中从所述JVM中取回调用栈信息的步骤包括:根据本地任务的ID和映射关系从所述JVM中取回与该本地任务对应的Java线程的调用栈信息。
图6是示出了步骤340的处理的一个示例的示意图。图6的处理对应于在多线程程序下在线程启动时建立了映射数据库的情况。首先,在步骤1中,辅助线程接收到来自内核的信号,该信号包含被阻塞的本地任务的ID。为了便于理解,以图4的示例进行说明,这里假定接收到的本地任务ID是5901。然后,在步骤2中,辅助线程查询预先建立的映射数据库,例如表1所示的数据结构。在本地任务ID是5901的情况下,从映射数据库中查到对应的Java线程ID(在表1的情况下为2)。也就是说,辅助线程得到内核的通知:Java应用线程2在内核中被阻塞。然后,在步骤3中,辅助线程根据查到的Java线程ID(即2),从JVM中的与Java应用线程2对应的栈取回调用栈信息。
具体地,可以利用JVMTI提供的GetFrameLocation()方法获得指定线程的栈的当前执行到的方法的方法名和位置。然后,利用获得的方法名调用JVMTI提供的GetLineNumberTable()方法,获得当前执行到的方法的位置和行号的对应表。通过该表可以查出现在该线程运行到所述方法的哪一行,从而实现了定位到Java源代码中的对应位置。所述对应位置可以被显示给调试人员,也可以被保存起来以供以后进行瓶颈分析。
最后,描述一种特殊情况的处理。本领域技术人员了解,与普通的Java应用线程一样,在本发明中创建的辅助线程同样是Java线程,并且Java应用线程和辅助线程位于同一进程内,例如,如图4的情况所示。此外,辅助线程同样对应于内核空间中的一个本地任务。另一方面,在步骤330中,在探测器即j_switch_to函数中进行监视的目标是进程,即监视被调出的本地任务是否属于作为监视目标的进程。如上所述,这是通过检查是否满足条件(1)而实现的。因此,当辅助线程自身被阻塞时,由于在探测器中会检测到同时满足了条件(1)以及条件(2),所以在这种情况下探测器也会向辅助线程发送信号。但是,这种信号是无用的,与我们想要监视的Java程序的源代码本身涉及到瓶颈的部分没有关系,需要忽略这种信号。
可以采用多种方式来忽略由辅助线程本身被阻塞而引发的信号。例如,至少可以采用以下两种方法。
第一种方法是在探测器中进行额外的判断。除了条件(1)调出的本地任务属于PID指示的进程以及(2)调出的本地任务处于阻塞状态这两个条件之外,进一步设置条件(3):调出的本地任务的ID与对应于辅助线程的本地任务的ID不同,即,被调出的本地任务不对应用户空间的辅助线程。然后,在同时满足这三个条件的情况下,才向辅助线程发送信号。
第二种方法是在辅助线程中进行判断。当辅助线程从操作系统内核接收到包含被阻塞的本地任务的本地任务ID时(图6中的步骤1),辅助线程查询预先建立的映射数据库(图6中的步骤2),例如表1所示的数据结构。在假定本地任务ID是6012的情况下,从映射数据库中查到对应的Java线程ID(在表1的情况下为21)。辅助线程将获得的Java线程ID与自身的Java线程ID进行比较,当这两者匹配时,即表示辅助线程自身在内核中被阻塞。此时,辅助线程忽略该信号,跳过图6中的步骤3的执行。
在上文中,详细描述了根据本发明的实施例的方法流程300。方法流程300适用于单核处理器的情况。
本发明的用于检测并定位JAVA程序的瓶颈的方法同样适用于多核处理器的情况。在执行Java程序的处理器是多核处理器的情况下,创建多个辅助线程。图7是示出了在四核处理器的情况下的辅助线程示例的示意图。在图7中,创建的辅助线程的数目与多核处理器的核数目相同。即,在四核处理器的情况下,创建了四个辅助线程1至4。然后,将这四个辅助线程中的每一个分别绑定到多核处理器的一个核。即,将辅助线程1绑定到处理器核1,将辅助线程2绑定到处理器核2,将辅助线程3绑定到处理器核3,并且将辅助线程4绑定到处理器核4。
实现上述功能,需要对方法流程中的步骤310进行如下修改。
在回调函数vmInit()中,根据处理器核的数目,调用JVMTI的RunAgentThread()方法创建相等的数目的辅助线程。然后,通过JNI接口提供的AttachCurrentThread()方法将每个运行的当前辅助线程挂接到JVM上,让其可以访问JVM的堆栈信息。将这些辅助线程设置为较高的调度优先级。然后,调用系统调用函数sched_setaffinity(),将每个线程绑定到一个处理器核上。这样,四个辅助线程被一一对应地绑定到四个处理器核上,从而可以按照与单核处理器上的单个辅助线程相似的方式执行处理。
需要注意的是,四核处理器仅仅是一个示例。本发明同样适用于双核处理器、八核处理器以及具有更多核的处理器。
采用本发明的上述方法,能够将本地层中的瓶颈准确地链接回Java源代码,即找到引起所述本地层中的瓶颈的Java源代码的相应位置。因此,上述方法能够在JVM层中没有任何指示的情况下,找到Java线程状态改变的原因。此外,上述方法是独立且自足的方法,不需要其它的监视器或工具的帮助。另外,上述方法由于采用信号机制,不会在每次方法被调用时都记录栈信息,所以没有明显的性能开销,不会对目标应用程序的正常运行产生不利影响。
本领域技术人员会认识到,可以以方法、系统或计算机程序产品的形式提供本发明的实施例。因此,本发明可采取全硬件实施例、全软件实施例,或者组合软件和硬件的实施例的形式。硬件和软件的典型的结合可以是带有计算机程序的通用计算机系统,当程序被加载并被执行时,控制计算机系统,从而可以执行上述的方法。
本发明可以嵌入在计算机程序产品中,它包括使此处描述的方法得以实施的所有特征。所述计算机程序产品被包含在一个或多个计算机可读存储介质(包括,但不限于,磁盘存储器、CD-ROM、光学存储器等)中,所述计算机可读存储介质具有包含于其中的计算机可读程序代码。
已参考根据本发明的方法、系统及计算机程序产品的流程图和/或方框图说明了本发明。流程图和/或方框图中的每个方框,以及流程图和/或方框图中的方框的组合显然可由计算机程序指令实现。这些计算机程序指令可被提供给通用计算机、专用计算机、嵌入式处理器或者其他可编程的数据处理设备的处理器,以产生一台机器,从而指令(所述指令通过计算机或者其他可编程数据处理设备的处理器)产生用于实现在流程图和/或方框图的一个或多个方框中规定的功能的装置。
这些计算机程序指令也可保存在一个或多个计算机的读存储器中,每个这种存储器能够指挥计算机或者其他可编程数据处理设备按照特定的方式发挥作用,从而保存在计算机可读存储器中的指令产生一种制造产品,所述制造产品包括实现在流程图和/或方框图的一个或多个方框中规定的功能的指令装置。
计算机程序指令也可被加载到一个或多个计算机或者其他可编程数据处理设备上,使得在所述计算机或者其他可编程数据处理设备上执行一系列的操作步骤,从而在每个这样的设备上产生计算机实现的过程,以致在该设备上执行的指令提供用于实现在流程图和/或方框图的一个或多个方框中规定的步骤。
以上结合本发明的实施方式对本发明的原理进行了说明,但这些说明只是示例性的,不应理解为对本发明的任何限制。本领域技术人员可以对本发明进行各种改变和变形,而不会背离由随附权利要求所限定的本发明的精神和范围。