java开发怎么调试app_每个开发人员都应了解的5种高级Java调试技术

java开发怎么调试app

生产调试很难,而且越来越难。 随着架构变得更加分散,代码变得更加异步,准确定位和解决生产中发生的错误已不再是孩子的游戏。 在本文中,我们将讨论一些先进的技术,这些技术可以帮助您更快地了解生产中令人痛苦的错误的根本原因,而又不会给已经很忙的生产环境增加实质性的开销。

更好的jstack。 jstack已经存在很长时间了(大约与JVM一样存在),直到今天,它仍然是每个开发人员手中的重要工具。 每当您盯着一个挂起或没有响应的Java进程时,它都是查看每个线程的堆栈跟踪信息的首选工具。 即便如此,jstack也有一些缺点,这有损其帮助您调试生产中复杂问题的能力。 第一个是,虽然它通过堆栈跟踪告诉您每个线程在做什么,但并没有告诉您为什么在执行它(通常只能通过调试器获得的信息)。 而且它不会告诉您何时开始执行操作。

幸运的是,您可以通过向每个线程的数据中注入动态变量状态来解决此问题,并使它成为一个更好的工具,这是一种很棒的方法。 该问题的关键在于最意外的地方之一。 您会看到,jstack在执行的任何时候都不会查询JVM或应用程序代码以显示要显示的变量状态。 但是,有一个重要的例外,我们可以利用它将柠檬变成柠檬水,那就是-Thread.Name()属性,该属性的值被注入到堆栈转储中。 通过正确设置它,您可以摆脱像这样的无用的jstack线程打印输出

Object.wait()中的“ pool-1-thread-1”#17 prio = 5 os_prio = 31 tid = 0x00007f9d620c9800 nid = 0x6d03 [0x000000013ebcc000]

将其与以下线程打印输出进行比较,该输出包含对线程正在完成的实际工作的描述,传递给该线程的输入参数以及该线程开始处理该请求的时间:

” pool-1-thread-#17:队列:ACTIVE_PROD,消息ID:AB5CAD,类型:分析,事务ID:56578956,开始时间:2014年10月8日18:34”

这是我们如何设置有状态线程名称的示例:

private void processMessage(Message message) { //an entry point into your code
  String name = Thread.currentThread().getName();
  try {
    Thread.currentThread().setName(prettyFormat(name, getCurrTranscationID(), 
                 message.getMsgType(), message.getMsgID(), getCurrentTime()));
    doProcessMessage(message);
  }
  finally {
    Thread.currentThread().setName(name); // return to original name 
  }
}

在此示例中,线程正在处理队列外的消息,我们将看到线程正在从队列中使消息出队的目标队列,正在处理的消息的ID以及与之相关的事务(即对于本地复制至关重要),最后但并非最不重要-处理此消息的时间。 这最后一点信息使您可以查看具有多达一百个工作线程的服务器jstack,并查看哪些线程最先启动并且最有可能导致应用程序服务器挂起。

(点击图片放大)

一个增强的jstack如何显示转储中每个线程的动态变量状态的示例。 线程开始时间标记为TS。

当您使用探查器,商业监视工具,JMX控制台甚至Java 8的新Mission Control时,该功能也同样有效。 在所有这些情况下,具有状态线程上下文将显着增强查看活动线程状态或历史线程转储,并准确查看每个线程在做什么以及何时启动的能力。

任何JDK或商业调试器或分析器还将显示此线程变量状态。

但是,线程名称的值并不止于此。 它们在生产调试中起着更大的作用-即使您根本不使用jstack。 一个实例是全局Thread.uncaughtExceptionHandler回调,该回调充当未捕获的异常终止线程(或将其发送回线程池)之前的最后一道防线。

到到达未捕获的异常处理程序时,代码执行已停止,并且帧和变量状态都已回滚。 您可以记录日志的唯一状态是线程正在处理的任务,它的参数和启动时间是您猜到的-状态线程名称(以及加载到其上的所有其他TLS变量)。

重要的是要记住,线程框架可能在不知道的情况下隐式捕获异常。 ThreadPoolExecutorService是一个很好的例子,它可以捕获Runnable中的所有异常并将它们委托给它的afterExecute方法,您可以重写该方法以显示有意义的内容。 因此,无论使用哪种框架,请注意,如果线程失败,您仍然有机会记录异常和线程状态,以避免任务消失在以太中。

吞吐量和死锁jstacks 。 jstack或Mission Control之类的工具的另一个缺点是,需要在遇到问题的目标JVM上手动激活它们。 这会降低其在生产中的效率,因为在生产中,有99%的时间发生问题时都不需要调试。

足够令人高兴的是,有一种方法可以使您的应用程序的吞吐量低于特定阈值或满足特定条件时,以编程方式激活jstack。 您甚至可以在JVM发生死锁时自动激活jstack,以准确查看哪些线程正在死锁以及其他所有线程在做什么(当然,每个线程都与动态变量状态耦合,这要感谢有状态线程名)。 由于死锁和并发问题在大多数情况下都是零星的,而且众所周知很难复制,因此这是非常宝贵的。 在这种情况下,通过在死锁时刻自动激活jstack,它还包含每个线程的状态信息,可以极大地促进您再现和解决此类bug的能力。

单击此处以了解有关如何从应用程序内自动检测死锁的更多信息。

捕获实时变量 。 我们已经讨论了通过线程上下文从JVM捕获状态的方法。 这种方法尽管有效,但仅限于必须事先格式化为线程名称的变量。 理想情况下,我们希望能够从实时JVM的代码中的任意位置获取任何变量的值,而无需附加调试器或重新部署代码。 一个伟大的工具已经存在了一段时间,但是还没有得到应有的认可,您可以这样做。 该工具是BTrace。

BTrace是一个有用的工具,可让您在实时JVM上运行类似Java的脚本来捕获或聚集任何形式的变量状态,而无需重新启动JVM或部署新代码。 这使您能够执行非常强大的功能,例如打印线程的堆栈跟踪记录,写入特定文件或打印任何队列或连接池的项目数等等。

这是使用BTrace脚本完成的,BTrace脚本是一种类似于Java的语法,您可以在其中编写函数,这些函数通过字节码转换(选择过程将在下面进行介绍)插入到您选择的位置的代码中。 试用该工具的最佳方法是将其示例脚本附加到实时应用程序中。 用法非常简单,只需从命令行输入-btrace <JVM pid> <脚本名称>。无需重新启动JVM。

BTrace的文档非常齐全,并且随附许多示例脚本(请参见下文),涵盖了有关IO,内存分配和类加载的各种常见调试方案。 这是您可以使用BTrace轻松完成的一些强大示例-

  • NewArray.java :每当分配了新的char []时进行打印,并根据其值添加您自己的条件。 对于选择性内存分析非常方便。
  • FileTracker.java :每当应用程序写入特定文件位置时,进行打印。 非常适合查明IO操作过多的原因。
  • Classload.java :每当将目标类加载到JVM时都进行React。 对于调试“地狱般”的情况非常有用。

BTrace被设计为非侵入性工具,这意味着它无法更改应用程序状态或流控制。 这是一件好事,因为它减少了我们对实时代码执行产生负面影响的机会,并使其在生产中的使用更加容易接受。 但是此功能有一些严格的限制-您不能创建对象(甚至不是字符串!),也不能调用您或第三方的代码(以执行诸如日志记录之类的操作),甚至无法做一些简单的事情(例如循环)而担心创建无限循环 为了做到这些,您必须使用下一组技术-编写自己的Java代理。

Java代理是一个jar文件,通过JVM提供对Instrumentation对象的访问,使您能够修改已加载到JVM中的字节码以更改其行为。 这实际上使您可以“重写” JVM已加载和编译的代码,而无需重新启动应用程序或更改磁盘上的.class文件。 就像在类固醇上使用BTrace一样思考-您基本上可以将应用程序中任何位置的新代码注入代码和第三方代码中,以捕获所需的任何信息。

编写自己的代理的最大缺点是,与BTrace不同,BTrace允许您编写类似Java的脚本来捕获状态,而Java代理则在字节码级别运行。 这意味着,如果要将代码注入应用程序,则必须创建正确的字节码。 这可能很棘手,因为遵循类似于操作员堆栈的语法(在许多方面类似于汇编语言),很难生成和读取字节码。 更麻烦的是,由于字节码已被编译,因此与JVM的验证者无缘无故地将拒绝与字节码注入位置的任何不相关。

为此,多年来编写了许多字节码生成库,例如JavaAssistASM (这是我个人最喜欢的)。 一个很棒的技巧,我发现我经常使用ASM byteview IDE 插件 ,该插件可让您在编辑器中键入任何Java代码,并自动生成正确的ASM代码,然后生成等效的字节代码,然后将其复制并粘贴到您的代理商。

单击此处,获取示例代理程序的真实示例,我们使用该示例程序来检测和散发来自服务器上第三方代码的内存泄漏-将其与应用程序状态相关联。

使用ASM字节码插件从IDE动态生成字节码生成脚本。

我要简要介绍的最后一项技术是构建本机JVM代理 。 这种方法使用了JVM TI C ++ API层,它为您提供了前所未有的控制和对JVM内部的访问。 这包括诸如在GC启动和停止时获取回调,在生成新线程时获取,获取监视器以及更多其他低级功能。 到目前为止,这是从运行代码中获取状态的最强大方法,因为您实际上是在JVM级别运行的。

但是,强大的力量伴随着巨大的责任,并且一些相当复杂的挑战使得这种方法相对难以实施。 首先,由于您是在JVM级别运行的,因此不再使用跨平台Java编写代码,而是使用依赖于底层平台的C ++编写代码。 第二个缺点是,API本身虽然功能非常强大,但是却难以使用,并且会严重影响性能,具体取决于所使用的特定功能集。

从好的方面来说,如果正确使用此层,则可以对JVM的各个部分进行出色的访问,否则,在我们寻找生产错误的根本原因时,这些层将对我们关闭。 当我们开始编写Takipi进行生产调试时,我不确定我们是否知道TI在我们构建工具的能力中扮演关键角色的程度。 这样做的原因是,通过使用此层,您可以检测异常,调用OS或映射应用程序代码,而无需手动用户操作。 如果您有时间研究该API层-我强烈建议您使用它,因为它为我们打开了一个众所周知的JVM的独特窗口。

翻译自: https://www.infoq.com/articles/Advanced-Java-Debugging-Techniques/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java开发怎么调试app

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值