Как стать автором
Обновить

Необычная Java: StackTrace Extends Throwable

Время на прочтение 10 мин
Количество просмотров 7K
Автор оригинала: Peter Lawrey

Прочтите эту статью и узнайте о необычных вещах в Java, которые могут оказаться на удивление полезными.

Есть вещи, которые вы можете делать в Java, но вы их редко видите. В основном потому, что в них нет смысла. Однако в Java есть несколько необычных вещей, которые могут оказаться на удивление полезными.

Chronicle Software обычно использует ряд различных шаблонов в своих низкоуровневых библиотеках, с которыми большинство разработчиков вообще не сталкивается.

Один из них — это класс, который расширяет Throwable, но не является ошибкой или исключением.

StackTrace Extends 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;
   }
}

Некоторые важные примечания, чтобы для начала:

  • Это не тот класс исключения, которое, как я  рассчитываю, когда-либо может возникнуть. Классы, непосредственно расширяющие 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. Это возможно потому, что ваш объект Logger будет поддерживать выдачу трассировки стека. например, вы можете кликнуть по номеру строки, чтобы просмотреть код, создавший ее.

Мониторинг производительности критического потока в рабочей среде

В некоторых средах вам нужен способ мониторинга критических событий в рабочей среде с низкими издержками без запуска профилировщика. 

Этого можно добиться, включив свой собственный мониторинг, чтобы производить трассировку стека только тогда, когда превышен некоторый порог. Это поможет найти проблемы, которые вы не можете воспроизвести в среде тестирования или разработки, поэтому это может быть бесценно.

После того как мы добавили эту возможность в нашу инфраструктуру, количество мистических задержек, о которых сообщили нам наши клиенты, резко сократилось, поскольку клиенты могли сами диагностировать проблему по трассировке стека.

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 не требует специальной обработки, поскольку logger будет игнорировать Throwable, который имеет значение null, и вы можете указать null как причину для исключения, и это то же самое, что и не указывать ее.

Заключение

Хотя класс, который напрямую расширяет Throwable, выглядит странно, он разрешен.

Более того он удивительно полезен для предоставления дополнительной информации о жизненном цикле ресурса или добавления простого мониторинга, который вы можете запустить в рабочей среде.

Ссылки

Chronicle Open Source

OpenHFT Chronicle Core

Теги:
Хабы:
+17
Комментарии 1
Комментарии Комментарии 1

Публикации

Истории

Работа

Java разработчик
359 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн