当前位置: 首页 > 科技观察

JVM源码分析的Attach机制实现了对

时间:2023-03-23 01:42:27 科技观察

Attach是什么的完整解读。说这个之前,我们先从一个大家都知道的事情说起。当我们感觉线程卡在了某个地方,想知道卡在哪里的时候,首先想到的就是进行threaddump,而常用的命令是jstack,我们可以看到如下线程栈。大家有没有注意到上面圈出来的两个线程,“AttachListener”和“SignalDispatcher”,这两个线程是我们先讲的Attach机制的关键,先偷偷告诉大家。其实jvm起来的时候AttachListener线程可能不存在,后面会细说。什么是附加机制?简单来说,jvm提供了一种jvm进程之间的通信能力,可以让一个进程向另一个进程传递命令,让其进行一些内部操作。比如为了让另一个jvm进程dump线程,那么我们跑了一个jstack进程,然后传递一个pid参数,告诉它由哪个进程进行threaddump。既然是两个进程,那肯定涉及到进程间通信和传输协议的定义,比如执行什么操作,传递什么参数等等,Attach可以做的事情总结一下,比如memorydump、threaddump、类信息统计(比如加载的类和大小、以及实例数等)、代理的动态加载(用过btrace的应该不陌生)、动态设置vmflag(但不是所有的flag都可以设置,因为jvm启动过程中会用到一些flag,而且是一次性的),打印vmflag,获取系统属性等,这些对应的源码(AttachListener.cpp)如下staticAttachOperationFunctionInfofuncs[]={{"agentProperties",get_agent_properties},{"datadump",data_dump},{"dumpheap",dump_heap},{"load",JvmtiExport::load_agent_library},{"properties",get_system_properties},{"threaddump",thread_dump},{"inspectheap",heap_inspection},{"setflag",set_flag},{"printflag",print_flag},{"jcmd",jcmd},{NULL,NULL}};下面是命令对应的处理函数。Attach是如何实现jvm中AttachListener线程的创建的,前面也有提到。jvm在启动过程中可能不会启动AttachListener线程。可以通过jvm参数启动。代码(Threads::create_vm)如下:){returntrue;}else{returnfalse;}whereDisableAttachMechanism,Start,AttachListenerReduceSignalUsage平均默认为false(globals.hpp)product(bool,DisableAttachMechanism,false,"DisablemechanismthatallowstoolstoAttachtothisVM")product(bool,StartAttachListener,false,"AlwaysstartAttachListeneratVMstartup")product(bool,ReduceSignalUsage,false,"ReducetheuseofOSsignalsinJavaand/ortheVM")因此AttachListener::init()不会被执行,AttachListener线程是在该方法中创建的。既然启动时不会创建这个线程,那我们上面看到的线程是怎么创建的呢?这个需要注意另一个线程“SignalDispatcher”。顾名思义,它处理信号。这个线程是jvm启动的时候创建的,具体代码就不说了。下面通过jstack的实现来说明触发Attach机制的过程。jstack命令的实现其实是一个叫做JStack.java的类。查看jstack代码后,会进入到下面的方法。请注意VirtualMachine.Attach(pid);这行代码是触发Attachpid的关键。如果是在linux下,你会去下面的构造函数中解释代码。首先,可以看到调用了createAttachFile方法,在目标进程的cwd目录下创建了一个文件/proc/。/cwd/.attach_pid,在后续的信号处理过程中会把这个取出来进行判断(为了安全),而我们知道Linux下线程是由进程实现的,在jvm启动的时候会创建很多线程,比如我们上面信号线程,也就是你会看到很多pid(应该是LWP),那么如何找到这个信号处理线程呢,从上面的实现来看,就是找到我们传入的pid的父进程,然后给所有的子进程都发送一个SIGQUIT信号,而jvm中除了信号线程外,其他线程都对这个信号设置了屏蔽,所以接收不到信号,所以把信号传递给“SignalDispatcher”并在发送后轮询等待,看目标进程是否创建了文件。AttachTimeout默认的超时时间是5000ms,可以通过设置系统变量sun.tools.Attach.AttachTimeout来指定。下面是SignalDispatcher线程的入口实现。当信号为SIGBREAK时(在jvm#define中做,其实就是SIGQUIT),就会触发AttachListener::is_init_trigger()的执行。一开始会判断当前进程目录下是否有.Attach_pid文件(前面提到过),如果没有,就会在/tmp下创建一个/tmp/.Attach_pid,当文件的uid一致时使用自己的uid(为了安全),然后调用init方法。这时真相大白,创建了一个线程,命名为AttachListener。查看它的子类LinuxAttachListener的init方法可以看到它创建了一个监听socket并创建了一个文件/tmp/.java_pid。这个文件就是client之前一直轮询等待的文件。有了这个文件的生成,就意味着Attach过程成功完成。Attach监听器收到请求看其入口实现Attach_listener_thread_entry从代码上看,就是不断从队列中取AttachOperation,然后找到请求命令对应的方法执行,比如我们在上文提到的jstack命令开始,找到{"threaddump",thread_dump}的映射关系,然后执行thread_dump方法可以看到要调用的AttachListener::dequeue(),AttachOperation*AttachListener::dequeue(){JavaThread*thread=JavaThread::current();ThreadBlockInVMtbivm(thread);thread->set_suspend_equivalent();//clearedbyhandle_special_suspend_equivalent_condition()or//java_suspend_self()viacheck_and_wait_while_suspended()AttachOperation*op=LinuxAttachListener::dequeue();//werewerewereexternallysuspendedwhilewewerewaiting?thread->check_and_wait_while_suspended();returnop;}最后调用的是LinuxAttachListener::dequeue(),我们看到如果没有请求,它会一直在那里接受。当有请求到来时,就会创建一个socket,读取数据,构造一个LinuxAttachOperation返回执行。整个过程是这样的,从创建Attach线程到接收请求,处理请求。