不寻常的 Java:StackTrace 扩展了 Throwable

本文探讨了在Java中扩展Throwable类创建StackTrace类的不常见用法。StackTrace不仅记录异常发生的位置和线程,还能延迟捕获异常的原因,帮助诊断资源关闭和线程并发访问问题。通过监控关键线程的堆栈跟踪,可以有效地定位生产环境中的性能问题。此外,通过系统属性可以控制跟踪的开启和关闭,降低开销。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在 Java 中你可以做一些你很少看到的事情,通常是因为它没有用处。但是,Java 中有一些不寻常的东西可能会非常有用。

Chronicle Software在其低级库中使用了许多不同的常用模式,大多数开发人员通常不会遇到。

其中之一是扩展 Throwable 但不是错误或异常的类。

StackTrace 扩展了 Throwable

package net.openhft.chronicle.core;
/**
* Throwable created purely for the purposes of reporting a stack trace.
* This is not an Error or an Exception and is not expected to be thrown or caught.
*/
public class StackTrace extends Throwable {
   public StackTrace() { this("stack trace"); }
   public StackTrace(String message) { this(message, null); }
   public StackTrace(String message, Throwable cause) {
       super(message + " on " + Thread.currentThread().getName(), cause);
   }

   public static StackTrace forThread(Thread t) {
       if (t == null) return null;
       StackTrace st = new StackTrace(t.toString());
       StackTraceElement[] stackTrace = t.getStackTrace();
       int start = 0;
       if (stackTrace.length > 2) {
           if (stackTrace[0].isNativeMethod()) {
               start++;
           }
       }
      if (start > 0) {
         StackTraceElement[] ste2 = new StackTraceElement[stackTrace.length - start];
         System.arraycopy(stackTrace, start, ste2, 0, ste2.length);
         stackTrace = ste2;
      }

       st.setStackTrace(stackTrace);
       return st;
   }
}

一些重要的旁注先让开

  • 是的,我确实在我的 IDE 中使用了比例字体。我在 Windows 上使用 Verdana,我很容易习惯并且不想回去。
  • 这不是我期望被抛出的课程。检查直接扩展 Throwable 的类,就像 Exception 一样,因此编译器将帮助您执行此操作。
  • Throwable 的堆栈跟踪是在创建 Throwable 时确定的,而不是在抛出它的位置。通常,这是同一行,但并非必须如此。不必抛出 Throwable 即可获得堆栈跟踪。
  • 堆栈跟踪元素对象在需要时才会创建。相反,元数据被添加到对象本身以减少开销,并且在首次使用时填充 StackTraceElements 数组。

但是,让我们更详细地看一下这个类。该类将记录它创建位置的堆栈跟踪和创建它的线程。稍后您应该会看到这有什么用处。

它还可以用于保存另一个正在运行的线程的堆栈跟踪。仅当线程到达安全点时才会获取另一个线程的堆栈跟踪,这可能是在您尝试获取它之后的一段时间。这是由于 JVM 停止线程,并且通常 JVM 等待停止每个线程,因此它可以检查您尝试捕获的线程的堆栈。

即它有很高的开销,但可能非常有用。

StackTrace 作为延迟异常

我们不希望抛出这个 Throwable,但它可以记录稍后可能抛出的异常的原因。

为什么资源被关闭

public class EgMain {
   static class MyCloseable implements Closeable {
       protected transient volatile StackTrace closedHere;

       @Override
       public void close() {
           closedHere = new StackTrace("Closed here"); // line 13
       }

       public void useThis() {
           if (closedHere != null)
               throw new IllegalStateException("Closed", closedHere);
       }
   }

   public static void main(String[] args) throws InterruptedException {      
 MyCloseable mc = new MyCloseable(); // line 27
       Thread t = new Thread(mc::close, "closer");
       t.start();
       t.join();
       mc.useThis();
   }
}

运行时产生以下异常:


通常您会看到 IllegalStateException 以及您的代码尝试使用已关闭资源的位置,但这并不能告诉您为什么在没有其他信息的情况下关闭它。

由于 StackTrace 是 Throwable,您可以将其作为后续异常或错误的原因。 

您可以看到关闭资源的线程,因此您知道它发生在另一个线程中,并且您可以看到它关闭原因的堆栈跟踪。这有助于快速诊断过早关闭资源的难以发现的问题。

哪个资源被丢弃了?

长寿命的 Closeable 对象可能有一个复杂的生命周期,并且确保它们在需要时关闭可能难以追踪,并且可能导致资源泄漏。当 GC 释放对象时,某些资源不会被清理,例如 RandomAccessFile 对象在 GC 上被清理,除非您关闭它,否则它所代表的文件不会关闭,从而导致文件句柄的潜在资源泄漏。

public class CreatedMain {
   static class MyResource implements Closeable {
       private final transient StackTrace createdHere = new StackTrace("Created here");
       volatile transient boolean closed;

       @Override
       public void close() throws IOException {
           closed = true;
       }

       @Override
       protected void finalize() throws Throwable {
           super.finalize();
           if (!closed)
               Logger.getAnonymousLogger().log(Level.WARNING, "Resource discarded but not closed", createdHere);
       }
   }

   public static void main(String[] args) throws InterruptedException {
       new MyResource(); // line 27
       System.gc();
       Thread.sleep(1000);
   }
}

打印以下内容:

这使您不仅可以查看资源的创建位置,因此您可以尝试确定它未关闭的原因,而且以您的 IDE 理解的方式登录是微不足道的,因为您的记录器已经支持打印出堆栈跟踪. 例如,您可以单击行号来查看创建它的代码。

性能监控是生产中的关键线程

在某些环境中,您需要一种低开销的方式来监控生产中关键事件的抖动,而无需运行分析器。这可以通过添加您自己的监控来实现,以便仅在堆栈跟踪超过某个阈值时对其进行采样。这可以发现您无法在测试或开发环境中重现的问题,因此它非常宝贵。

当我们将其添加到我们的基础架构中时,客户向我们报告的神秘延迟数量急剧下降,因为客户可以从堆栈跟踪中自行诊断出问题所在。

public class JitteryMain implements Runnable {
   volatile long loopStartMS = Long.MIN_VALUE;
   volatile boolean running = true;

   @Override
   public void run() {
       while (running) {
           loopStartMS = System.currentTimeMillis();
           doWork();
           loopStartMS = Long.MIN_VALUE;
       }
   }

   private void doWork() {
       int loops = new Random().nextInt(100);
       for (int i = 0; i < loops; i++)
           pause(1); // line 24
   }

   static void pause(int ms) {
       try {
           Thread.sleep(ms); // line 29
       } catch (InterruptedException e) {
           throw new AssertionError(e); // shouldn't happen
       }
   }

   public static void main(String[] args) {
       final JitteryMain jittery = new JitteryMain();
       Thread thread = new Thread(jittery, "jitter");
       thread.setDaemon(true);
       thread.start();

       // monitor loop
       long endMS = System.currentTimeMillis() + 1_000;
       while (endMS > System.currentTimeMillis()) {
           long busyMS = System.currentTimeMillis() - jittery.loopStartMS;
           if (busyMS > 100) {
               Logger.getAnonymousLogger()
                       .log(Level.INFO, "Thread spent longer than expected here, was " + busyMS + " ms.",
                               StackTrace.forThread(thread));
           }
           pause(50);
       }
       jittery.running = false;
   }
}

打印以下内容,您可以再次看到很容易在 IDE 中导航堆栈。

您可能想知道为什么在这种情况下会发生这种情况。最可能的原因是Thread.sleep(time) 睡眠时间最短,而不是最长,并且在 Windows 上睡眠 1 毫秒实际上相当一致地需要大约 1.9 毫秒。

检测单线程资源何时在线程间并发访问

package net.openhft.chronicle.core;

public class ConcurrentUsageMain {
   static class SingleThreadedResource {
       private StackTrace usedHere;
       private Thread usedByThread;

       public void use() {
           checkMultithreadedAccess();
           // BLAH
       }

       private void checkMultithreadedAccess() {
           if (usedHere == null || usedByThread == null) {
               usedHere = new StackTrace("First used here");
               usedByThread = Thread.currentThread();
           } else if (Thread.currentThread() != usedByThread) {
               throw new IllegalStateException("Used two threads " + Thread.currentThread() + " and " + usedByThread, usedHere);
           }
       }
   }

   public static void main(String[] args) throws InterruptedException {
       SingleThreadedResource str = new SingleThreadedResource();
       final Thread thread = new Thread(() -> str.use(), "Resource user"); // line 25
       thread.start();
       thread.join();

       str.use(); // line 29
   }
}

打印以下内容:

您可以看到该资源已被两个线程及其名称使用,但是,您还可以看到它们在堆栈中用于确定可能原因的位置。

关闭此跟踪

创建 StackTrace 对线程和可能的 JVM 有重大影响。但是,使用系统属性等控制标志很容易将其关闭并替换为 值。

createdHere = Jvm.isResourceTracing() 
                        ? new StackTrace(getClass().getName() + " created here")
                        : null;

使用null 不需要太多特殊处理,因为记录器会忽略一个为null的 Throwable ,您可以为 Exception 提供一个null 原因,这与不提供一个原因相同。

结论

虽然有一个直接扩展 Throwable 的类令人惊讶,但它是允许的,并且对于提供有关资源生命周期的其他信息或添加可以在生产中运行的简单监控也非常有用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值