Arthas在处理应用启动时相关疑难杂症显得捉襟见肘,jdb是java命令行调试工具,具体的调试功能和IDE相关调试几乎一致,接下来试着玩一玩Arthas和jdb工具在一起能学到什么新的知识。
文章写的比较急促,大段代码无法完整显示。
准备
Arthas安装
在 Github 上找到最新的发布包:https://github.com/alibaba/arthas/releases
$ wget https://github.com/alibaba/arthas/releases/download/arthas-all-3.3.2/arthas-3.3.2-bin.zip$ unzip arthas-3.2.2-bin.zip -d arthas$ cd arthas# 帮助$ java -jar arthas-boot.jar -h
如何使用
# 进入交互页面$ java -jar arthas-boot.jar[INFO] arthas-boot version: 3.2.2[INFO] Process 62498 already using port 3658[INFO] Process 62498 already using port 8563[INFO] Found existing java process, please choose one and hit RETURN.* [1]: 62498 xxx Java 进程 xxx[2]: 54006 xxx Java 进程 xxx[3]: 53993 xxx Java 进程 xxx> 选择一个 Java 进程ID,进入交互页面[INFO] arthas home: ~/.arthas[INFO] The target process already listen port 3658, skip attach.[INFO] arthas-client connect 127.0.0.1 3658,---. ,------. ,--------.,--. ,--. ,---. ,---./ O \ | .--. ''--. .--'| '--' | / O \ ' .-'| .-. || '--'.' | | | .--. || .-. |`. `-.| | | || |\ \ | | | | | || | | |.-' |`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
启动任一你的应用,在启动参数中加入,具体参数可以参考其他资料
-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=y
这里主要提一下suspend=y,代表应用启动时会默认阻塞,等待jdb连接,一般IDE在启用debug模式时,suspend=n,自动连接到应用,比如IDEA的debug模式在log下会显示:
-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:56645,suspend=y,server=nConnected to the target VM, address: '127.0.0.1:56645', transport: 'socket'
在正常启动模式下,加入debug启动参数后,应用阻塞在启动方法中。
Listening for transport dt_socket at address: 8000
在很多需要从启动环节开始对进行应用诊断,可以在该节点启动Arthas,并提示选择对应的应用,阻塞在main方法的应用无法在提示列表正确显示对应的应用启动名,会显示。
-- main class information unavailable
选择对应的应用,Arthas会等待一段时间attach并提示。
[INFO] arthas home: /Users/min_xu/.arthas/lib/3.3.2/arthas[INFO] Try to attach process 1389[ERROR] Start arthas failed, exception stack trace:com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file: target process not responding or HotSpot VM not loaded at sun.tools.attach.BsdVirtualMachine.(BsdVirtualMachine.java:90) at sun.tools.attach.BsdAttachProvider.attachVirtualMachine(BsdAttachProvider.java:63) at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208) at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:84) at com.taobao.arthas.core.Arthas.(Arthas.java:28) at com.taobao.arthas.core.Arthas.main(Arthas.java:124)[ERROR] attach fail, targetPid: 1389
原因在于通过attach 系统管道通信连接到待调试的应用响应超时,此时,我们必须要快速进入jdb命令行工具进入交互环节。
min_xu@xumindeMacBook-Pro Downloads % jdb -connect com.sun.jdi.SocketAttach:port=8000,hostname=127.0.0.1设置未捕获的java.lang.Throwable设置延迟的未捕获的java.lang.Throwable正在初始化jdb...>VM 已启动: 当前调用堆栈上没有帧
此时,jdb调试工具正式连接到应用程序。
到此为止,Arthas + jdb完成了应用的连接,结束。
......
等等,今天我们想解决的是什么问题。
对,本地执行一直正常的程序打包到线上提示 NoSuchMethodError。
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'xxx': Injection of resource dependencies failed; nested exceptsocket/config/socketio/SocketIoConfig.class]: Bean instantiation via factory method failed; n ested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.corundumstudio.socketio.SocketIOServer]: Factory method 'socketIOServer' threw exception; nested exception is java.lang.NoSuchMethodError: io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel()Z at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:321)ion is
第一轮
启动时异常可以通过在main方法中进行阻塞式调用,防止进程退出同样可以达到目的。
public static void main(String[] args) throws IOException { try { SpringApplication.run(SocketApplication.class, args); } catch (Throwable e) { e.printStackTrace(); } // block 保证进程不退出 System.in.read(); }
然而今天的目的是为了玩一玩 jdb,使用jdb的阻塞机制完成Arthas的attach,为了完成这个功能,需要开启两个shell会话窗口。
首先,需要解决掉Arthas attach到应用超时异常问题,既然阻塞住了应用,那我们需要让程序跑来,没错 jdb命令行窗口
run
......
等等,程序直接运行到Error退出了。
既然是阻塞调用,我们先尝试给这个类执行错误之前打个断点,分析源码。
public final class ZlibCodecFactory { private static final int DEFAULT_JDK_WINDOW_SIZE = 15; private static final int DEFAULT_JDK_MEM_LEVEL = 8; private static final boolean noJdkZlibDecoder; private static final boolean noJdkZlibEncoder; private static final boolean supportsWindowSizeAndMemLevel; static { noJdkZlibDecoder = SystemPropertyUtil.getBoolean("io.netty.noJdkZlibDecoder", PlatformDependent.javaVersion() < 7); logger.debug("-Dio.netty.noJdkZlibDecoder: {}", noJdkZlibDecoder); noJdkZlibEncoder = SystemPropertyUtil.getBoolean("io.netty.noJdkZlibEncoder", false); logger.debug("-Dio.netty.noJdkZlibEncoder: {}", noJdkZlibEncoder); supportsWindowSizeAndMemLevel = noJdkZlibDecoder || PlatformDependent.javaVersion() >= 7; } /** * Returns {@code true} if specify a custom window size and mem level is supported. */ public static boolean isSupportingWindowSizeAndMemLevel() { return supportsWindowSizeAndMemLevel; }
ZlibCodecFactory类内部存在一个static 语句块,调用Error的是静态方法isSupportingWindowSizeAndMemLevel,按照类加载的7个步骤顺序,static语句块需要在类初始化阶段触发,而后被isSupportingWindowSizeAndMemLevel被调用,所以给static语句块 37行加上一个断点,重新上述的步骤。
第二轮
min_xu@xumindeMacBook-Pro Downloads % jdb -connect com.sun.jdi.SocketAttach:port=8000,hostname=127.0.0.1设置未捕获的java.lang.Throwable设置延迟的未捕获的java.lang.Throwable正在初始化jdb...>VM 已启动: 当前调用堆栈上没有帧> stop at io.netty.handler.codec.compression.ZlibCodecFactory:37正在延迟断点io.netty.handler.codec.compression.ZlibCodecFactory:37。将在加载类后设置。
在此启动Arthas,attach到对应的应用。
Arthas依旧阻塞在了attach过程中不能自拔,继续让jdb run。
#jdb run> run
第二轮,诊断程序执行到异常,退出!Arthas 停止阻塞进入到调试阶段,随着诊断程序的异常退出,Arthas紧跟着退出了。
当你绝望沮丧的时候,就是救星出场的时候,Arthas通过OS 管道通信连接到诊断应用,应用内部一定是通过某个非阻塞的线程完成,jdb断点是不是可以仅针对某个线程阻塞呢?答案是肯定的,jdb以及IDEA默认的debug阻塞默认都是所有线程 All Thread,在jdb中,改名了是suspend {threadId} + run 命令组合。
第三轮
这次我们根据jdb断点提示,把断点打在类加载的方法上(后面有解释)。
min_xu@xumindeMacBook-Pro Downloads % jdb -connect com.sun.jdi.SocketAttach:port=8000,hostname=127.0.0.1设置未捕获的java.lang.Throwable设置延迟的未捕获的java.lang.Throwable正在初始化jdb...>VM 已启动: 当前调用堆栈上没有帧main[1] stop in io.netty.handler.codec.compression.ZlibCodecFactory.()正在延迟断点io.netty.handler.codec.compression.ZlibCodecFactory.()。将在加载类后设置。main[1] threads #查看所有线程组system: (java.lang.ref.Reference$ReferenceHandler)0x156 Reference Handler正在执行条件等待 (java.lang.ref.Finalizer$FinalizerThread)0x155 Finalizer 正在执行条件等待 (java.lang.Thread)0x154 Signal Dispatcher正在运行组main: (java.lang.Thread)0x1 main 正在运行
Arthas 抓紧上车了,在另外一个Shell窗口启动Arthas,attach到诊断程序。
main[1] suspend 0x1main[1] run> resume 0x1> 设置延迟的断点io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker:54断点命中: "线程=main", io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.<init>(), 行=54 bci=0main[1] suspend 0x1main[1] runmain[1] print io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel()com.sun.tools.example.debug.expr.ParseException: Name unknown: io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() = 空值main[1] next>已完成的步骤: "线程=main", io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler.<init>(), 行=34 bci=28main[1] print io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() = truemain[1]
按照上述的逻辑,Arthas终于连接上诊断程序。
jdb分别断点在方法前后,Arthas分别完成了ZlibCodecFactory加载完成前后的调用,在ZlibCodecFactory加载完成后,可以清楚得看到ZlibCodecFactory加载的几乎所有细节内容。
[arthas@3157]$ jad io.netty.handler.codec.compression.ZlibCodecFactoryNo class found for: io.netty.handler.codec.compression.ZlibCodecFactoryAffect(row-cnt:0) cost in 18 ms.[arthas@3157]$ jad io.netty.handler.codec.compression.ZlibCodecFactoryNo class found for: io.netty.handler.codec.compression.ZlibCodecFactoryAffect(row-cnt:0) cost in 4 ms.[arthas@3157]$ jad io.netty.handler.codec.compression.ZlibCodecFactoryClassLoader:+-sun.misc.Launcher$AppClassLoader@18b4aac2 +-sun.misc.Launcher$ExtClassLoader@7f424ec2Location:/Users/xxxx/.m2/repository/io/netty/netty-codec/4.1.32.Final/netty-codec-4.1.32.Final.jar
一般静态语句块是一个jdb断点的好标记,如果没有static怎么办,如何定位到init的方法是关键。
stop in MyClass.<init> (<init>标识MyClass的构造函数)stop in MyClass.<clinit> (<clinit>标识MyClass的静态初始化代码)
查看一下完整的jdb调试过程。
查看下Arthas的完成操作过程,注意:这两个窗口是在交叉联调。
一次有意思的Arthas + jdb调试过程完成,最后回答一下自问问题,都已经能jdb debug了,还需要arthas干啥呢:arthas解决了更多的透明度工具。
jdb提供了一系列print工具,可以再jdb shell中查看
print支持多种包含方法调用的简单Java表达式,例如:print MyClass.myStaticFieldprint myObj.myInstanceFieldprint i + j + k (i, j, k are primities and either fields or local variables)print myObj.myMethod() (if myMethod returns a non-null)print new java.lang.String("Hello").length()
回到上面的问题,jdb在断点前后同样可以使用print工具查看内置的一些程序状态。
main[1] print io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel()com.sun.tools.example.debug.expr.ParseException: Name unknown: io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() = 空值main[1] next>已完成的步骤: "线程=main", io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler.<init>(), 行=34 bci=28main[1] print io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() io.netty.handler.codec.compression.ZlibCodecFactory.isSupportingWindowSizeAndMemLevel() = truemain[1]
那Arthas jad可以给我们提供哪些更有价值的信息呢?
[arthas@64343]$ sc -d io.netty.handler.codec.compression.ZlibCodecFactory class-info io.netty.handler.codec.compression.ZlibCodecFactory code-source /Users/min_xu/.m2/repository/io/netty/netty-codec/4.1.32.Final/netty-codec-4.1.32.Final.jar name io.netty.handler.codec.compression.ZlibCodecFactory isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name ZlibCodecFactory modifier final,public annotation interfaces super-class +-java.lang.Object class-loader +-java.net.URLClassLoader@108941f3 +-sun.misc.Launcher$AppClassLoader@18b4aac2 +-sun.misc.Launcher$ExtClassLoader@6e1e0d4c classLoaderHash 108941f3
如果需要查看更详细的Feild信息,增加-f
fields name logger type io.netty.util.internal.logging.InternalLogger modifier final,private,static value LocationAwareSlf4JLogger(io.netty.handler.codec.compression.ZlibCodecFactory) name DEFAULT_JDK_WINDOW_SIZE type int modifier final,private,static value 15 name DEFAULT_JDK_MEM_LEVEL type int modifier final,private,static value 8 name noJdkZlibDecoder type boolean modifier final,private,static value false name noJdkZlibEncoder type boolean modifier final,private,static value false name supportsWindowSizeAndMemLevel type boolean modifier final,private,static value true
在启动时可以通过Arthas查看更多案例,如果仅仅是类加载问题,可以更简单的使用JVM参数 -XX:+TraceClassLoading从日志中跟踪。
本次玩耍结束,祝玩得开心。
更多玩法:jdb help