Deploying your Java code to production limits your troubleshooting options. Connecting to your app in production with a debugger is usually out of the question, and you might not even be able to get console access. So even with monitoring, you’re going to end up troubleshooting many problems post-mortem. This means looking at logs and, if you’re lucky, working with a Java stack trace.
没错,我说如果您有堆栈跟踪记录,您很幸运。 这就像一次将指南针,地图和头等舱机票交给您一样! 让我们讨论一下Java堆栈跟踪是什么以及如何使用它。
什么是Java堆栈跟踪?
堆栈跟踪(也称为堆栈回溯或什至只是回溯)是堆栈帧的列表。 这些帧表示应用程序执行期间的时刻。 堆栈框架是有关代码调用的方法或函数的信息。 因此,Java堆栈跟踪是从当前方法开始并一直延伸到程序启动时的帧列表。
有时,堆栈与堆栈之间会产生混淆。 纸叠是一种数据结构,它就像桌上的纸叠一样:先入先出。 您将文档添加到文件堆中,然后按照与放置它们相反的顺序将它们取下。 堆栈,更准确地称为运行时或调用堆栈,是程序在执行时创建的一组堆栈框架,这些堆栈框架以堆栈数据结构组织。
让我们看一个例子。
Java堆栈跟踪示例
让我们看一下Java程序。 此类调用四个方法,并从最后一个方法向控制台打印堆栈跟踪。
公共类StackTrace { 公共静态void main(String [] args){ 一种(); } 静态void a(){ b(); } 静态void b(){ C(); } 静态void c(){ d(); } 静态void d(){ Thread.dumpStack(); } }
上课时,您会看到以下内容:
java.lang.Exception:堆栈跟踪 在java.base / java.lang.Thread.dumpStack(Thread.java:1383) 在com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:23) 在com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:19) 在com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:15) 在com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:11) 在com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:7)
d()method()位于堆栈的顶部,因为这是应用程序生成跟踪的位置。 main()方法位于底部,因为这是程序启动的地方。 程序启动时,Java运行时将执行main()方法。 Main()称为a()。 A()称为b(),b()称为c(),后者称为d()。 最后,d()称为dumpStack(),它生成输出。 此Java堆栈跟踪为我们提供了程序执行顺序的图片。
Java堆栈跟踪是一个瞬间的快照。 您可以看到应用程序在哪里以及如何到达那里。 这是宝贵的见解,您可以使用几种不同的方式。
如何使用Java堆栈跟踪
既然您已经看到了Java堆栈跟踪向您显示的内容,那么如何使用它们?
Java异常
堆栈跟踪和异常通常相互关联。 当您看到Java应用程序引发异常时,通常会看到与它一起记录的堆栈跟踪。 这是因为异常是如何工作的。
当Java代码引发异常时,运行时将在堆栈中查找具有可以处理该异常的处理程序的方法。 如果找到一个,它将异常传递给它。 如果不是,程序将退出。 因此,异常和调用堆栈直接链接在一起。 了解这种关系将有助于您弄清楚代码为什么引发异常。
让我们更改示例代码。
首先,修改d()方法:
静态void d(){ 抛出新的NullPointerException(“ Oops!”); }
然后,更改main()和a(),以便main可以捕获异常。 您需要向a()添加一个受检查的异常,以便代码可以编译。
公共静态void main(String [] args) { 尝试{ 一种(); }抓(InvalidClassException ice){ System.err.println(ice.getMessage()); } } 静态void a()抛出InvalidClassException { b(); }
您故意抓住“错误”例外。 运行此代码,然后观察会发生什么。
线程“主”中的异常java.lang.NullPointerException:糟糕! 在com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:29) 在com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:24) 在com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:20) 在com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:16) 在com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:9)
异常使通过main()的堆栈冒泡,因为您试图捕获其他异常。 因此,运行时将其抛出,从而终止了应用程序。 不过,您仍然可以看到堆栈跟踪,因此很容易确定发生了什么。
现在,更改main()以捕获NullPointerException。 您也可以从a()中删除已检查的异常。
公共静态void main(String [] args){ 尝试{ 一种(); }抓(NullPointerException ice){ System.err.println(ice.getMessage()); } } 静态void a(){ b(); }
重新运行程序。
糟糕!
我们丢失了堆栈跟踪! 通过仅打印附加到异常的消息,您错过了一些重要的上下文。 除非你记得你为什么写糟糕!在该消息中,查找此问题将变得很复杂。 让我们再试一次。
公共静态void main(String [] args){ 尝试{ 一种(); } catch(NullPointerException npe){ npe.printStackTrace(); } }
重新运行该应用程序。
java.lang.NullPointerException:糟糕! 在com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:28) 在com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:24) 在com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:20) 在com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:16) 在com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:9)
这样更好! 我们看到了堆栈跟踪,它在发生异常的d()处结束,即使main()打印了它。
记录Java堆栈跟踪
如果您不想将错误消息打印到控制台,而是打印到日志文件,该怎么办? 好消息是,如果使用正确的参数调用大多数记录器,包括Log4j和Logback,它们将使用堆栈跟踪编写异常。
Pass in the exception object as the last argument to the message, without a formatting directive. So if you used Log4j or Logback with the sample code like this:
logger.error(“发生了什么坏事:”,npe);
您会在日志文件中看到以下内容:
不好的事情发生了: java.lang.NullPointerException:糟糕! 在com.ericgoebelbecker.stacktraces.StackTrace.d(StackTrace.java:28) 在com.ericgoebelbecker.stacktraces.StackTrace.c(StackTrace.java:24) 在com.ericgoebelbecker.stacktraces.StackTrace.b(StackTrace.java:20) 在com.ericgoebelbecker.stacktraces.StackTrace.a(StackTrace.java:16) 在com.ericgoebelbecker.stacktraces.StackTrace.main(StackTrace.java:9)
您可以对异常和堆栈跟踪进行的最好的操作之一就是记录它们,以便可以使用它们来隔离问题。 如果您习惯打印有用的日志消息,其中包含堆栈跟踪和日志索引等详细信息,则搜索工具(例如Scalyr)将成为故障排除工具包中最强大的工具之一。
Java调试器
调试器通过控制程序的运行时并让您观察和控制它来进行工作。 为此,它向您显示了程序堆栈,并使您可以在任一方向上对其进行遍历。 在调试器中,与查看日志消息中的堆栈跟踪信息相比,您可以获得更完整的堆栈框架图。
让我们进行一些小的代码更改,然后将示例代码放入调试器。
首先,将局部变量添加到d()方法中:
静态void d(){ 字符串消息=“糟糕”。 抛出新的NullPointerException(message); }
然后添加一个断点,其中d()在调试器中引发异常。 我正在为此图像使用IntelliJ的调试器。
在这里您可以看到我们添加到d()的字符串是堆栈框架的一部分,因为它是一个局部变量。 调试器在Stack内部运行,并为您提供每帧的详细图片。
强制线程转储
线程转储是很棒的事后分析工具,但是它们对于运行时问题也很有用。 如果您的应用程序停止响应或消耗的CPU或内存超出您的预期,则可以通过以下方式检索有关正在运行的应用程序的信息:jstack。
修改main()以便应用程序运行直到被杀死:
公共静态void main(String [] args)引发异常{ 尝试{ while(true){ Thread.sleep(1000); } }抓(NullPointerException ice){ ice.printStackTrace(); } }
运行该应用程序,确定其pid,然后运行jstack。 在Windows上,您需要按Ctrl中断在DOS窗口中,您正在其中运行代码。
$ jstack <pid>
Jstack将产生大量输出。
too long
我的应用程序正在运行11个线程,而jstack为所有这些线程生成了堆栈跟踪。 第一个线程(有用地命名为main)是我们关注的那个线程。 您可以看到它在wait()上处于休眠状态。
Java堆栈跟踪:您的路线图
A stack trace is more than just a picture inside your application. It's a snapshot of a moment in time that includes every step your code took to get there. There's no reason to dread seeing one in your logs because they're a gift from Java that tells you exactly what happened. Make sure you're logging them when an error crops up and send them to a tool like Scalyr so they're easy to find.
Now that you understand what a Java stack trace is and how to use it, take a look at your code. Are you throwing away critical information about errors and exceptions in your code? Is there a spot where a call to Thread.dumpstack() might help you isolate a recurring bug? Perhaps it's time to run your app through the debugger a few times with some strategically-chosen breakpoints.