Stack trace что это ошибки

Простыми словами, трассировка стека – это список методов, которые были вызваны до момента, когда в приложении произошло исключение.

Простой случай

В указанном примере мы можем точно определить, когда именно произошло исключение. Рассмотрим трассировку стека:

Exception in thread "main" java.lang.NullPointerException
       at com.example.myproject.Book.getTitle(Book.java:16)
       at com.example.myproject.Author.getBookTitles(Author.java:25)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

Это пример очень простой трассировки. Если пойти по списку строк вида «at…» с самого начала, мы можем понять, где произошла ошибка. Мы смотрим на верхний вызов функции. В нашем случае, это:

at com.example.myproject.Book.getTitle(Book.java:16)

Для отладки этого фрагмента открываем Book.java и смотрим, что находится на строке 16:

public String getTitle() {
    System.out.println(title.toString()); <-- line 16
    return title;
}

Это означает то, что в приведенном фрагменте кода какая-то переменная (вероятно, title) имеет значение null.

Пример цепочки исключений

Иногда приложения перехватывают исключение и выбрасывают его в виде другого исключения. Обычно это выглядит так:

try {
    ....
} catch (NullPointerException e) {
    throw new IllegalStateException("A book has a null property", e)
}

Трассировка в этом случае может иметь следующий вид:

Exception in thread "main" java.lang.IllegalStateException: A book has a null property
       at com.example.myproject.Author.getBookIds(Author.java:38)
       at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
       at com.example.myproject.Book.getId(Book.java:22)
       at com.example.myproject.Author.getBookIds(Author.java:35)
       ... 1 more

В этом случае разница состоит в атрибуте «Caused by» («Чем вызвано»). Иногда исключения могут иметь несколько секций «Caused by». Обычно необходимо найти исходную причину, которой оказывается в самой последней (нижней) секции «Caused by» трассировки. В нашем случае, это:

Caused by: java.lang.NullPointerException <-- root cause
       at com.example.myproject.Book.getId(Book.java:22) <-- important line

Аналогично, при подобном исключении необходимо обратиться к строке 22 книги Book.java, чтобы узнать, что вызвало данное исключение – NullPointerException.

Еще один пугающий пример с библиотечным кодом

Как правило, трассировка имеет гораздо более сложный вид, чем в рассмотренных выше случаях. Приведу пример (длинная трассировка, демонстрирующая несколько уровней цепочек исключений):

javax.servlet.ServletException: Произошло что–то ужасное
   at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:60)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at com.example.myproject.ExceptionHandlerFilter.doFilter(ExceptionHandlerFilter.java:28)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at com.example.myproject.OutputBufferFilter.doFilter(OutputBufferFilter.java:33)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
   at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
   at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
   at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
   at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
   at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
   at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
   at org.mortbay.jetty.Server.handle(Server.java:326)
   at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
   at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
   at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
   at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
   at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
   at org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)
   at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
Caused by: com.example.myproject.MyProjectServletException
   at com.example.myproject.MyServlet.doPost(MyServlet.java:169)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
   at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
   at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
   at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
   at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:30)
   ... 27 more
Caused by: org.hibernate.exception.ConstraintViolationException: could not insert: [com.example.myproject.MyEntity]
   at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:96)
   at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
   at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:64)
   at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2329)
   at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2822)
   at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:71)
   at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
   at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:321)
   at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
   at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
   at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:56)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
   at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:50)
   at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
   at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:705)
   at org.hibernate.impl.SessionImpl.save(SessionImpl.java:693)
   at org.hibernate.impl.SessionImpl.save(SessionImpl.java:689)
   at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
   at java.lang.reflect.Method.invoke(Method.java:597)
   at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
   at $Proxy19.save(Unknown Source)
   at com.example.myproject.MyEntityService.save(MyEntityService.java:59) <-- relevant call (see notes below)
   at com.example.myproject.MyServlet.doPost(MyServlet.java:164)
   ... 32 more
Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
   at org.hsqldb.jdbc.Util.throwError(Unknown Source)
   at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
   at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
   at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:57)
   ... 54 more

В этом примере приведен далеко не полный стек вызовов. Что вызывает здесь наибольший интерес, так это поиск функций из нашего кода – из пакета com.example.myproject. В предыдущем примере мы сначала хотели отыскать «первопричину», а именно:

Caused by: java.sql.SQLException

Однако все вызовы методов в данном случае относятся к библиотечному коду. Поэтому мы перейдем к предыдущей секции «Caused by» и найдем первый вызов метода из нашего кода, а именно:

at com.example.myproject.MyEntityService.save(MyEntityService.java:59)

Аналогично предыдущим примерам, необходимо обратить внимание на MyEntityService.java, строка 59: именно здесь появилась ошибка (в данном случае ситуация довольно очевидная, так как об ошибке сообщает SQLException, но в этом вопросе мы рассматриваем именно процедуру отладки с помощью трассировки).

Перевод ответа: «What is a stack trace, and how can I use it to debug my application errors?» @Rob Hruska

Практика показала, что хардкорные расшифровки с наших докладов хорошо заходят, так что мы решили продолжать. Сегодня у нас в меню смесь из подходов к поиску и анализу ошибок и крэшей, приправленная щепоткой полезных инструментов, подготовленная на основе доклада Андрея Паньгина aka apangin из Одноклассников на одном из JUG’ов (это была допиленная версия его доклада с JPoint 2016). В без семи минут двухчасовом докладе Андрей подробно рассказывает о стек-трейсах и хип-дампах.

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

Сегодня я буду рассказывать про стек-трейсы и хип-дампы — тему, с одной стороны, известную каждому, с другой — позволяющую постоянно открывать что-то новое (я даже багу нашел в JVM, пока готовил эту тему).

Когда я делал тренировочный прогон этого доклада у нас в офисе, один из коллег спросил: «Все это очень интересно, но на практике это кому-нибудь вообще полезно?» После этого разговора первым слайдом в свою презентацию я добавил страницу с вопросами по теме на StackOverflow. Так что это актуально.

Сам я работаю ведущим программистом в Одноклассниках. И так сложилось, что зачастую мне приходится работать с внутренностями Java — тюнить ее, искать баги, дергать что-то через системные классы (порой не совсем легальными способами). Оттуда я и почерпнул большую часть информации, которую сегодня хотел вам представить. Конечно, в этом мне очень помог мой предыдущий опыт: я 6 лет работал в Sun Microsystems, занимался непосредственно разработкой виртуальной Java-машины. Так что теперь я знаю эту тему как изнутри JVM, так и со стороны пользователя-разработчика.

Стек-трейсы

Стек-трейсы exception

Когда начинающий разработчик пишет свой «Hello world!», у него выскакивает эксепшн и ему демонстрируется стек-трейс, где произошла эта ошибка. Так что какие-то представления о стек-трейсах есть у большинства.

Перейдем сразу к примерам.

Я написал небольшую программку, которая в цикле 100 миллионов раз производит такой эксперимент: создает массив из 10 случайных элементов типа long и проверяет, сортированный он получился или нет.

package demo1;

import java.util.concurrent.ThreadLocalRandom;

public class ProbabilityExperiment {

    private static boolean isSorted(long[] array) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] > array[i + 1]) {
                return false;
            }
        }
        return true;
    }

    public void run(int experiments, int length) {
        int sorted = 0;

        for (int i = 0; i < experiments; i++) {
            try {
                long[] array = ThreadLocalRandom.current().longs(length).toArray();
                if (isSorted(array)) {
                    sorted++;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        System.out.printf("%d of %d arrays are sortedn", sorted, experiments);
    }

    public static void main(String[] args) {
        new ProbabilityExperiment().run(100_000_000, 10);
    }
}

По сути он считает вероятность получения сортированного массива, которая приблизительно равна 1/n!. Как это часто бывает, в программке ошиблись на единичку:

for (int i = 0; i < array.length; i++)

Что произойдет? Эксепшн, выход за пределы массива.
Давайте разбираться, в чем дело. У нас в консоль выводится:

java.lang.ArrayIndexOutOfBoundsException

но стек-трейсов никаких нет. Куда делись?

В HotSpot JVM есть такая оптимизация: у эксепшенов, которые кидает сама JVM из горячего кода, а в данном случае код у нас горячий — 100 миллионов раз дергается, стек-трейсы не генерируются.
Это можно исправить с помощью специального ключика:

-XX:-OmitStackTraceInFastThrow

Теперь попробуем запустить пример. Получаем все то же самое, только все стек-трейсы на месте.

Подобная оптимизация работает для всех неявных эксепшенов, которые бросает JVM: выход за границы массива, разыменование нулевого указателя и т.д.

Раз оптимизацию придумали, значит она зачем-то нужна? Понятно, что программисту удобнее, когда стек-трейсы есть.

Давайте измерим, сколько «стоит» у нас создание эксепшена (сравним с каким-нибудь простым Java-объектом, вроде Date).

@Benchmark
public Object date() {
    return new Date();
}

@Benchmark
public Object exception() {
    return new Exception();
}

С помощью JMH напишем простенькую бенчмарку и измерим, сколько наносекунд занимают обе операции.

Оказывается, создать эксепшн в 150 раз дороже, чем обычный объект.
И тут не все так просто. Для виртуальной машины эксепшн не отличается от любого другого объекта, но разгадка кроется в том, что практически все конструкторы эксепшн так или иначе сводятся к вызову метода fillInStackTrace, который заполняет стек-трейс этого эксепшена. Именно заполнение стек-трейса отнимает время.

Этот метод в свою очередь нативный, падает в VM рантайм и там гуляет по стеку, собирает все фреймы.

Метод fillInStackTrace публичный, не final. Давайте его просто переопределим:

@Benchmark
public Object exceptionNoStackTrace() {
    return new Exception() {
        @Override
        public Throwable fillInStackTrace() {
            return this;
        }
    };
}

Теперь создание обычного объекта и эксепшена без стек-трейса отнимают одинаковое время.

Есть и другой способ создать эксепшн без стек-трейса. Начиная с Java 7, у Throwable и у Exception есть protected-конструктор с дополнительным параметром writableStackTrace:

protected Exception(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace);

Если туда передать false, то стек-трейс генерироваться не будет, и создание эксепшена будет очень быстрым.

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

А сколько стоит бросить эксепшн?

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

@Param("1", "2", "10", "100", "1000"})
int depth;

@Benchmark
public Object throwCatch() {
    try {
        return recursive(depth);
    } catch (Exception e) {
        return e;
    }
}

Вот, что дают измерения:

Т.е. если у нас глубина небольшая (эксепшн ловится в том же фрейме или фреймом выше — глубина 0 или 1), эксепшн ничего не стоит. Но как только глубина стека становится большой, затраты совсем другого порядка. При этом наблюдается четкая линейная зависимость: «стоимость» исключения почти линейно зависит от глубины стека.

Дорого стоит не только получение стек-трейса, но и дальнейшие манипуляции — распечатка, отправка по сети, запись, — все, для чего используется метод getStackTrace, который переводит сохраненный стек-трейс в объекты Java.

@Benchmark
public Object fillInStackTrace() {
    return new Exception();
}

@Benchmark
public Object getStackTrace() {
    return new Exception().getStackTrace();
}

Видно, что преобразование стек-трейса в 10 раз «дороже» его получения:

Почему это происходит?

Вот метод getStackTrace в исходниках JDK:

Сначала через вызов нативного метода мы узнаем глубину стека, потом в цикле до этой глубины вызываем нативный метод, чтобы получить очередной фрейм и сконвертировать его в объект StackTraceElement (это нормальный объект Java с кучей полей). Мало того, что это долго, процедура отнимает много памяти.

Более того, в Java 9 этот объект дополнен новыми полями (в связи с известным проектом модуляризации) — теперь каждому фрейму приписывается отметка о том, из какого он модуля.

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

Давайте подведем итоги

  • создание самого объекта эксепшн — дешевое;
  • занимает время получение его стек-трейса;
  • еще дороже — преобразование этого внутреннего стек-трейса в Java-объект в StackTraceElement. Сложность этого дела прямо пропорциональна глубине стека.
  • бросание эксепшн — быстрое, оно почти ничего не стоит (почти как безусловный переход),
  • но только если эксепшн ловится в том же фрейме. Тут надо дополнить еще, что JIT у нас умеет инлайнить методы, поэтому один скомпилированный фрейм может в себя включать несколько Java-методов, заинлайниных друг в друга. Но если эксепшн ловится где-то глубже по стеку, его дороговизна пропорциональна глубине стека.

Пара советов:

  • отключайте на продакшене оптимизацию, возможно, это сэкономит много времени на отладке:
    -XX:-OmitStackTraceInFastThrow

  • не используйте эксепшены для управления потоком программы; это считается не очень хорошей практикой;
  • но если вы все-таки прибегаете к этому способу, позаботьтесь о том, чтобы эксепшены были быстрыми и лишний раз не создавали стек-трейсы.

Стек-трейсы в тред дампах

Чтобы узнать, что же делает программа, проще всего взять тред дамп, например, утилитой jstack.

Фрагменты вывода этой утилиты:

Что здесь видно? Какие есть потоки, в каком они состоянии и их текущий стек.

Более того, если потоки захватили какие-то локи, ожидают входа в synchronized-секцию или взятия ReentrantLock, это также будет отражено в стек-трейсе.

Порой полезным оказывается малоизвестный идентификатор:

Он напрямую связан с ID потока в операционной системе. Например, если вы смотрите программой top в Linux, какие треды у вас больше всего едят CPU, pid потока — это и есть тот самый nid, который демонстрируется в тред дампе. Можно тут же найти, какому Java-потоку он соответствует.

В случае с мониторами (с synchronized-объектами) прямо в тред дампе будет написано, какой тред и какие мониторы держит, кто пытается их захватить.

В случае с ReentrantLock это, к сожалению, не так. Здесь видно, как Thread 1 пытается захватить некий ReentrantLock, но при этом не видно, кто этот лок держит. На этот случай в VM есть опция:

-XX:+PrintConcurrentLocks

Если мы запустим то же самое с PrintConcurrentLocks, в тред дампе увидим и ReentrantLock.

Здесь указан тот самый id лока. Видно, что его захватил Thread 2.

Если опция такая хорошая, почему бы ее не сделать «по умолчанию»?

Она тоже чего-то стоит. Чтобы напечатать информацию о том, какой поток какие ReentrantLock’и держит, JVM пробегает весь Java heap, ищет там все ReentrantLock’и, сопоставляет их с тредами и только потом выводит эту информацию (у треда нет информации о том, какие локи он захватил; информация есть только в обратную сторону — какой лок связан с каким тредом).

В указанном примере по названиям потоков (Thread 1 / Thread 2) непонятно, к чему они относятся. Мой совет из практики: если у вас происходит какая-то длинная операция, например, сервер обрабатывает клиентские запросы или, наоборот, клиент ходит к нескольким серверам, выставляйте треду понятное имя (как в случае ниже — прямо IP того сервера, к которому клиент сейчас идет). И тогда в дампе потока сразу будет видно, ответа от какого сервера он сейчас ждет.

Хватит теории. Давайте опять к практике. Этот пример я уже не раз приводил.

package demo2;

import java.util.stream.IntStream;

public class ParallelSum {
    static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);

    public static void main(String[] args) {
        System.out.println(SUM);
    }
}

Запускаем программку 3 раза подряд. 2 раза она выводит сумму чисел от 0 до 100 (не включая 100), третий — не хочет. Давайте смотреть тред дампы:

Первый поток оказывается RUNNABLE, выполняет наш reduce. Но смотрите, какой интересный момент: Thread.State вроде как RUNNABLE, но при этом написано, что поток in Object.wait().

Мне тоже это было не понятно. Я даже хотел сообщить о баге, но оказывается, такая бага заведена много лет назад и закрыта с формулировкой: «not an issue, will not fix».
В этой программке действительно есть дедлок. Его причина — инициализация классов.

Выражение выполняется в статическом инициализаторе класса ParallelSum:

static int SUM = IntStream.range(0, 100).parallel().reduce(0, (x, y) -> x + y);

Но поскольку стрим параллельный, исполнение происходит в отдельных потоках ForkJoinPool, из которых вызывается тело лямбды:

(x, y) -> x + y

Код лямбды записан Java-компилятором прямо в классе ParallelSum в виде приватного метода. Получается, что из ForkJoinPool мы пытаемся обратиться к классу ParallelSum, который в данный момент находится на этапе инициализации. Поэтому потоки начинают ждать, когда же закончится инициализация класса, а она не может закончиться, поскольку ожидает вычисления этой самой свертки. Дедлок.

Почему вначале сумма считалась? Просто повезло. У нас небольшое количество элементов суммируется, и иногда все исполняется в одном потоке (другой поток просто не успевает).

Но почему же тогда поток в стек-трейсе RUNNABLE? Если почитать документацию к Thread.State, станет понятно, что никакого другого состояния здесь быть не может. Не может быть состояния BLOCKED, поскольку поток не заблокирован на Java-мониторе, нет никакой synchronized-секции, и не может быть состояния WAITING, потому что здесь нет никаких вызовов Object.wait(). Синхронизация происходит на внутреннем объекте виртуальной машины, который, вообще говоря, даже не обязан быть Java-объектом.

Стек-трейс при логировании

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

В Java нет препроцессора, поэтому нет возможности использовать макросы __FILE__, __LINE__, как в С (эти макросы еще на этапе компиляции преобразуются в текущее имя файла и строку). Поэтому других способов дополнить вывод именем файла и номером строки кода, откуда это было напечатано, кроме как через стек-трейсы, нет.

public static String getLocation() {
    StackTraceElement s = new Exception().getStackTrace()[2];
	return s.getFileName() + ':' + s.getLineNumber();
}

Генерим эксепшн, у него получаем стек-трейс, берем в данном случае второй фрейм (нулевой — это метод getLocation, а первый — вызывает метод warning).

Как мы знаем, получение стек-трейса и, тем более, преобразование его в стек-трейс элементы очень дорого. А нам нужен один фрейм. Можно ли как-то проще сделать (без эксепшн)?

Помимо getStackTrace у исключения есть метод getStackTrace объекта Thread.

Thread.current().getStackTrace()

Будет ли так быстрее?

Нет. JVM никакой магии не делает, здесь все будет работать через тот же эксепшн с точно таким же стек-трейсом.

Но хитрый способ все-таки есть:

public static String getLocation() {
    StackTraceElement s = sun.misc.SharedSecrets.getJavaLangAccess()
            .getStackTraceElement(new Exception(), 2);

    return s.getFileName() + ':' + s.getLineNumber();
}

Я люблю всякие приватные штуки: Unsafe, SharedSecrets и т.д.

Есть аксессор, который позволяет получить StackTraceElement конкретного фрейма (без необходимости преобразовывать весь стек-трейс в Java-объекты). Это будет работать быстрее. Но есть плохая новость: в Java 9 это работать не будет. Там проделана большая работа по рефакторингу всего, что связано со стек-трейсами, и таких методов там теперь просто нет.

Конструкция, позволяющая получить какой-то один фрейм, может быть полезна в так называемых Caller-sensitive методах — методах, чей результат может зависеть от того, кто их вызывает. В прикладных программах с такими методами приходится сталкиваться нечасто, но в самой JDK подобных примеров немало:

В зависимости от того, кто вызывает Class.forName, поиск класса будет осуществляться в соответствующем класс-лоадере (того класса, который вызвал этот метод); аналогично — с получением ResourceBundle и загрузкой библиотеки System.loadLibrary. Также информация о том, кто вызывает, полезна при использовании различных методов, которые проверяют пермиссии (а имеет ли данный код право вызывать этот метод). На этот случай в «секретном» API предусмотрен метод getCallerClass, который на самом деле является JVM-интринсиком и вообще почти ничего не стоит.

sun.reflect.Reflection.getCallerClass

Как уже много раз говорилось, приватный API — это зло, использовать которое крайне не рекомендуется (сами рискуете нарваться на проблемы, подобные тем, что ранее вызвал Unsafe). Поэтому разработчики JDK задумались над тем, что раз этим пользуются, нужна легальная альтернатива — новый API для обхода потоков. Основные требования к этому API:

  • чтобы можно было обойти только часть фреймов (если нам нужно буквально несколько верхних фреймов);
  • возможность фильтровать фреймы (не показывать ненужные фреймы, относящиеся к фреймворку или системным классам);
  • чтобы эти фреймы конструировались ленивым образом (lazy) — если нам не нужно получать информацию о том, с каким файлом он связан, эта информация преждевременно не извлекается;
  • как в случае с getCallerClass — нам нужно не имя класса, а сам инстанс java.lang.Class.

Известно, что в публичном релизе Java 9 будет java.lang.StackWalker.
Получить его экземпляр очень просто — методом getInstance. У него есть несколько вариантов — дефолтный StackWalker или незначительно конфигурируемый опциями:

  • опция RETAIN_CLASS_REFERENCE означает, что вам нужны не имена классов, а именно инстансы;
  • прочие опции позволяют показать в стек-трейсе фреймы, относящиеся к системным классам и классам рефлекшн (по умолчанию они не будут показаны в стек-трейсе).

Также для оптимизации можно задавать примерную глубину, которая необходима (чтобы JVM могла оптимизировать получение стек-фреймов в batch).

Простейший пример, как этим пользоваться:

StackWalker sw = StackWalker.getInstance();
sw.forEach(System.out::println);

Берем StackWalker и вызываем метод forEach, чтобы он обошел все фреймы. В результате получим такой простой стек-трейс:

То же самое с опцией SHOW_REFLECT_FRAMES:

StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_REFLECT_FRAMES);
sw.forEach(System.out::println);

В этом случае добавятся методы, относящиеся к вызову через рефлекшн:

Если добавить опцию SHOW_HIDDEN_FRAMES (она, кстати, включает в себя SHOW_REFLECT_FRAMES, т.е. рефлекшн-фреймы тоже будут показаны):

StackWalker sw = StackWalker.getInstance(StackWalker.Option.SHOW_HIDDEN_FRAMES);
sw.forEach(System.out::println);

В стек-трейсе появятся методы динамически-сгенерированных классов лямбд:

А теперь самый главный метод, который есть в StackWalker API — метод walk с такой хитрой непонятной сигнатурой с кучей дженериков:

public <T> T walk(Function<? super Stream<StackFrame>, ? extends T> function)

Метод walk принимает функцию от стек-фрейма.

Его работу проще показать на примере.

Несмотря на то, что все это выглядит страшно, как этим пользоваться — очевидно. В функцию передается стрим, а уже над стримом можно проводить все привычные операции. К примеру, вот так выглядел бы метод getCallerFrame, который достает только второй фрейм: пропускаются первые 2, потом вызывается findFirst:

public static StackFrame getCallerFrame() {
    return StackWalker.getInstance()
            .walk(stream -> stream.skip(2).findFirst())
            .orElseThrow(NoSuchElementException::new);
}

Метод walk возвращает тот результат, который возвращает эта функция стрима. Все просто.
Для данного конкретного случая (когда нужно получить просто Caller класс) есть специальный shortcut метод:

return StackWalker.getInstance(RETAIN_CLASS_REFERENCE).getCallerClass();

Еще один пример посложнее.

Обходим все фреймы, оставляем только те, которые относятся к пакету org.apache, и выводим первые 10 в список.

StackWalker sw = StackWalker.getInstance();

List<StackFrame> frames = sw.walk(stream ->
        stream.filter(sf -> sf.getClassName().startsWith("org.apache."))
              .limit(10)
              .collect(Collectors.toList()));

Интересный вопрос: зачем такая длинная сигнатура с кучей дженериков? Почему бы просто не сделать у StackWalker метод, который возвращает стрим?

public Stream<StackFrame> stream();

Если дать API, который возвращает стрим, у JDK теряется контроль над тем, что дальше над этим стримом делают. Можно дальше этот стрим положить куда-то, отдать в другой поток, попробовать его использовать через 2 часа после получения (тот стек, который мы пытались обойти, давно потерян, а тред может быть давно убит). Таким образом будет невозможно обеспечить «ленивость» Stack Walker API.

Основной поинт Stack Walker API: пока вы находитесь внутри walk, у вас зафиксировано состояние стека, поэтому все операции на этом стеке можно делать lazy.

На десерт еще немного интересного.

Как всегда, разработчики JDK прячут от нас кучу сокровищ. И помимо обычных стек-фреймов они для каких-то своих нужд сделали живые стек-фреймы, которые отличаются от обычных тем, что имеют дополнительные методы, позволяющие не только получить информацию о методе и классе, но еще и о локальных переменных, захваченных мониторах и значениях экспрешн-стека данного стек-фрейма.

/* package-private */
interface LiveStackFrame extends StackFrame {

    public Object[] getMonitors();
    public Object[] getLocals();
    public Object[] getStack();
    public static StackWalker getStackWalker();
}

Защита здесь не ахти какая: класс просто сделали непубличным. Но кто же нам мешает взять рефлекшн и попробовать его? (Примечание: в актуальных сборках JDK 9 доступ к непубличному API через рефлекшн запрещён. Чтобы его разрешить, необходимо добавить опцию JVM --add-opens=java.base/java.lang=ALL-UNNAMED)

Пробуем на таком примере. Есть программа, которая рекурсивным методом ищет выход из лабиринта. У нас есть квадратное поле size x size. Есть метод visit с текущими координатами. Мы пытаемся из текущей клетки пойти влево / вправо / вверх / вниз (если они не заняты). Если дошли из правой-нижней клетки в левую-верхнюю, считаем, что нашли выход и распечатываем стек.

package demo3;

import java.util.Random;

public class Labyrinth {
    static final byte FREE = 0;
    static final byte OCCUPIED = 1;
    static final byte VISITED = 2;

    private final byte[][] field;

    public Labyrinth(int size) {
        Random random = new Random(0);
        field = new byte[size][size];

        for (int x = 0; x < size; x++) {
            for (int y = 0; y < size; y++) {
                if (random.nextInt(10) > 7) {
                    field[x][y] = OCCUPIED;
                }
            }
        }

        field[0][0] = field[size - 1][size - 1] = FREE;
    }

    public int size() {
        return field.length;
    }

    public boolean visit(int x, int y) {
        if (x == 0 && y == 0) {
            StackTrace.dump();
            return true;
        }

        if (x < 0 || x >= size() || y < 0 || y >= size() || field[x][y] != FREE) {
            return false;
        }

        field[x][y] = VISITED;
        return visit(x - 1, y) || visit(x, y - 1) || visit(x + 1, y) || visit(x, y + 1);
    }

    public String toString() {
        return "Labyrinth";
    }

    public static void main(String[] args) {
        Labyrinth lab = new Labyrinth(10);
        boolean exitFound = lab.visit(9, 9);
        System.out.println(exitFound);
    }
}

Запускаем:

Если я делаю обычный dumpStack, который был еще в Java 8, получаем обычный стек-трейс, из которого ничего не понятно. Очевидно — рекурсивный метод сам себя вызывает, но интересно, на каком шаге (и с какими значениями координат) вызывается каждый метод.

Заменим стандартный dumpStack на наш StackTrace.dump, который через рефлекшн использует live стек-фреймы:

package demo3;

import java.lang.reflect.Method;
import java.util.Arrays;

public class StackTrace {

    private static Object invoke(String methodName, Object instance) {
        try {
            Class<?> liveStackFrame = Class.forName("java.lang.LiveStackFrame");
            Method m = liveStackFrame.getMethod(methodName);
            m.setAccessible(true);
            return m.invoke(instance);
        } catch (ReflectiveOperationException e) {
            throw new AssertionError("Should not happen", e);
        }
    }

    public static void dump() {
        StackWalker sw = (StackWalker) invoke("getStackWalker", null);
        sw.forEach(frame -> {
            Object[] locals = (Object[]) invoke("getLocals", frame);
            System.out.println(" at " + frame + "  " + Arrays.toString(locals));
        });
    }
}

В первую очередь надо получить соответствующий StackWalker, вызвав метод getStackWalker. Все фреймы, которые будут передаваться в getStackWalker, на самом деле будут экземплярами лайв стек-фрейма, у которого есть дополнительные методы, в частности, getLocals для получения локальных переменных.

Запускаем. Получаем то же самое, но у нас отображается весь путь из лабиринта в виде значений локальных переменных:


На этом мы заканчиваем первую часть поста. Вторая часть здесь.

Лично встретиться с Андреем в Москве можно будет уже совсем скоро — 7-8 апреля на JPoint 2017. В этот раз он выступит с докладом «JVM-профайлер с чувством такта», в котором расскажет, как можно получить честные замеры производительности приложения, комбинируя несколько подходов к профилированию. Доклад будет «всего» часовой, зато в дискуссионной зоне никто не будет ограничивать вас от вопросов и горячих споров!

Кроме этого, на JPoint есть целая куча крутых докладов практически обо всем из мира Java — обзор планируемых докладов мы давали в другом посте, а просто программу конференции вы найдете на сайте мероприятия.

In simple terms, a stack trace is a list of the method calls that the application was in the middle of when an Exception was thrown.

Simple Example

With the example given in the question, we can determine exactly where the exception was thrown in the application. Let’s have a look at the stack trace:

Exception in thread "main" java.lang.NullPointerException
        at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

This is a very simple stack trace. If we start at the beginning of the list of «at …», we can tell where our error happened. What we’re looking for is the topmost method call that is part of our application. In this case, it’s:

at com.example.myproject.Book.getTitle(Book.java:16)

To debug this, we can open up Book.java and look at line 16, which is:

15   public String getTitle() {
16      System.out.println(title.toString());
17      return title;
18   }

This would indicate that something (probably title) is null in the above code.

Example with a chain of exceptions

Sometimes applications will catch an Exception and re-throw it as the cause of another Exception. This typically looks like:

34   public void getBookIds(int id) {
35      try {
36         book.getId(id);    // this method it throws a NullPointerException on line 22
37      } catch (NullPointerException e) {
38         throw new IllegalStateException("A book has a null property", e)
39      }
40   }

This might give you a stack trace that looks like:

Exception in thread "main" java.lang.IllegalStateException: A book has a null property
        at com.example.myproject.Author.getBookIds(Author.java:38)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
        at com.example.myproject.Book.getId(Book.java:22)
        at com.example.myproject.Author.getBookIds(Author.java:36)
        ... 1 more

What’s different about this one is the «Caused by». Sometimes exceptions will have multiple «Caused by» sections. For these, you typically want to find the «root cause», which will be one of the lowest «Caused by» sections in the stack trace. In our case, it’s:

Caused by: java.lang.NullPointerException <-- root cause
        at com.example.myproject.Book.getId(Book.java:22) <-- important line

Again, with this exception we’d want to look at line 22 of Book.java to see what might cause the NullPointerException here.

More daunting example with library code

Usually stack traces are much more complex than the two examples above. Here’s an example (it’s a long one, but demonstrates several levels of chained exceptions):

javax.servlet.ServletException: Something bad happened
    at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:60)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.example.myproject.ExceptionHandlerFilter.doFilter(ExceptionHandlerFilter.java:28)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at com.example.myproject.OutputBufferFilter.doFilter(OutputBufferFilter.java:33)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
    at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388)
    at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216)
    at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182)
    at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765)
    at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418)
    at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152)
    at org.mortbay.jetty.Server.handle(Server.java:326)
    at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542)
    at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
    at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
    at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
    at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
    at org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228)
    at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
Caused by: com.example.myproject.MyProjectServletException
    at com.example.myproject.MyServlet.doPost(MyServlet.java:169)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
    at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
    at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166)
    at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:30)
    ... 27 more
Caused by: org.hibernate.exception.ConstraintViolationException: could not insert: [com.example.myproject.MyEntity]
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:96)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:64)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2329)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2822)
    at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:71)
    at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268)
    at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:321)
    at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
    at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
    at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:56)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
    at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:50)
    at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
    at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:705)
    at org.hibernate.impl.SessionImpl.save(SessionImpl.java:693)
    at org.hibernate.impl.SessionImpl.save(SessionImpl.java:689)
    at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
    at $Proxy19.save(Unknown Source)
    at com.example.myproject.MyEntityService.save(MyEntityService.java:59) <-- relevant call (see notes below)
    at com.example.myproject.MyServlet.doPost(MyServlet.java:164)
    ... 32 more
Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
    at org.hsqldb.jdbc.Util.throwError(Unknown Source)
    at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
    at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
    at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:57)
    ... 54 more

In this example, there’s a lot more. What we’re mostly concerned about is looking for methods that are from our code, which would be anything in the com.example.myproject package. From the second example (above), we’d first want to look down for the root cause, which is:

Caused by: java.sql.SQLException

However, all the method calls under that are library code. So we’ll move up to the «Caused by» above it, and in that «Caused by» block, look for the first method call originating from our code, which is:

at com.example.myproject.MyEntityService.save(MyEntityService.java:59)

Like in previous examples, we should look at MyEntityService.java on line 59, because that’s where this error originated (this one’s a bit obvious what went wrong, since the SQLException states the error, but the debugging procedure is what we’re after).

From Wikipedia, the free encyclopedia

«Backtrace» redirects here. For the 2018 film, see Backtrace (film).

In computing, a stack trace (also called stack backtrace[1] or stack traceback[2]) is a report of the active stack frames at a certain point in time during the execution of a program. When a program is run, memory is often dynamically allocated in two places; the stack and the heap. Memory is continuously allocated on a stack but not on a heap, thus reflective of their names. Stack also refers to a programming construct, thus to differentiate it, this stack is referred to as the program’s function call stack. Technically, once a block of memory has been allocated on the stack, it cannot be easily removed as there can be other blocks of memory that were allocated before it. Each time a function is called in a program, a block of memory called an activation record is allocated on top of the call stack. Generally, the activation record stores the function’s arguments and local variables. What exactly it contains and how it’s laid out is determined by the calling convention.

Programmers commonly use stack tracing during interactive and post-mortem debugging. End-users may see a stack trace displayed as part of an error message, which the user can then report to a programmer.

A stack trace allows tracking the sequence of nested functions called — up to the point where the stack trace is generated. In a post-mortem scenario this extends up to the function where the failure occurred (but was not necessarily caused). Sibling calls do not appear in a stack trace.

Language support[edit]

Many programming languages, including Java[3] and C#,[4] have built-in support for retrieving the current stack trace via system calls. Before std::stacktrace was added in standard library as a container for std::stacktrace_entry, pre-C++23 has no built-in support for doing this, but C++ users can retrieve stack traces with (for example) the stacktrace library. In JavaScript, exceptions hold a stack property that contain the stack from the place where it was thrown.

Python[edit]

As an example, the following Python program contains an error.

def a():
    i = 0
    j = b(i)
    return j

def b(z):
    k = 5
    if z == 0:
        c()
    return k + z

def c():
    error()

a()

Running the program under the standard Python interpreter produces the following error message.

Traceback (most recent call last):
  File "tb.py", line 15, in <module>
    a()
  File "tb.py", line 3, in a
    j = b(i)
  File "tb.py", line 9, in b
    c()
  File "tb.py", line 13, in c
    error()
NameError: name 'error' is not defined

The stack trace shows where the error occurs, namely in the c function. It also shows that the c function was called by b, which was called by a, which was in turn called by the code on line 15 (the last line) of the program. The activation records for each of these three functions would be arranged in a stack such that the a function would occupy the bottom of the stack and the c function would occupy the top of the stack.

Java[edit]

In Java, stack traces can be dumped manually with Thread.dumpStack()[5] Take the following input:

public class Main {
  public static void main(String args[]) {
    demo();
  }
  static void demo() {
    demo1();
  }
  static void demo1() {
    demo2();
  }
  static void demo2() {
    demo3();
  }
  static void demo3() {
    Thread.dumpStack();
  }
}

The exception lists functions in descending order, so the most-inner call is first.

java.lang.Exception: Stack trace
        at java.lang.Thread.dumpStack(Thread.java:1336)
        at Main.demo3(Main.java:15)
        at Main.demo2(Main.java:12)
        at Main.demo1(Main.java:9)
        at Main.demo(Main.java:6)
        at Main.main(Main.java:3)

C and C++[edit]

Both C and C++ (pre-C++23) do not have native support for obtaining stack traces, but libraries such as glibc and boost provide this functionality.[6][7] In these languages, some compiler optimizations may interfere with the call stack information that can be recovered at runtime. For instance, inlining can cause missing stack frames, tail call optimizations can replace one stack frame with another, and frame pointer elimination can prevent call stack analysis tools from correctly interpreting the contents of the call stack.[6]

For example, glibc’s backtrace() function returns an output with the program function and memory address.

./a.out() [0x40067f]
./a.out() [0x4006fe]
./a.out() [0x40070a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f7e60738f45]
./a.out() [0x400599]

As of C++23, stack traces can be dumped manually by printing the value returned by static member function std::stacktrace::current():[8]

std::cout << std::stacktrace::current() << 'n';

Rust[edit]

Rust has two types of errors. Functions that use the panic macro are «unrecoverable» and the current thread will become poisoned experiencing stack unwinding. Functions that return a std::result::Result are «recoverable» and can be handled gracefully.[9] However, recoverable errors cannot generate a stack trace as they are manually added and not a result of a runtime error.

As of June 2021, Rust has experimental support for stack traces on unrecoverable errors. Rust supports printing to stderr when a thread panics, but it must be enabled by setting the RUST_BACKTRACE environment variable.[10]

When enabled, such backtraces look similar to below, with the most recent call first.

thread 'main' panicked at 'execute_to_panic', main.rs:3
stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
   1: std::panicking::default_hook::{{closure}}
   2: std::panicking::default_hook
   3: std::panicking::rust_panic_with_hook
   4: std::panicking::begin_panic
   5: futures::task_impl::with
   6: futures::task_impl::park
...

See also[edit]

  • Tail call
  • Context (computing)
  • Stack overflow
  • Exception handling
  • Call stack

References[edit]

  1. ^ «libc manual: backtraces». gnu.org. Retrieved 8 July 2014.
  2. ^ «traceback — Print or retrieve a stack traceback». python.org. Retrieved 8 July 2014.
  3. ^ «Thread (Java SE 16 & JDK 16)». Java Platform Standard Edition & Java Development Kit Version 16 API Specification. 2021-03-04. Retrieved 2021-07-04.
  4. ^ «Environment.StackTrace Property (System)». Microsoft Docs. 2021-05-07. Retrieved 2021-07-04.
  5. ^ «Thread (Java Platform SE 8 )». docs.oracle.com. Retrieved 2021-06-15.
  6. ^ a b «Backtraces (The GNU C Library)». www.gnu.org. Retrieved 2021-06-15.
  7. ^ «Getting Started — 1.76.0». www.boost.org. Retrieved 2021-06-15.
  8. ^ «Working Draft, Standard for Programming Language C++» (PDF). open-std.org. ISO/IEC. 2021-10-23. p. 766.{{cite web}}: CS1 maint: url-status (link)
  9. ^ «rustonomicon unwinding — Rust». doc.rust-lang.org.
  10. ^ «std::backtrace — Rust». doc.rust-lang.org. Retrieved 2021-06-15.

As a developer, stack traces are one of the most common error types you’ll run into. Every developer makes mistakes, including you. When you make a mistake, your code will likely exit and print a weird-looking message called a stack trace.

But actually, a stack trace represents a path to a treasure, like a pirate map. It shows you the exact route your code traversed leading up to the point where your program printed an exception.

But, how do you read a stack trace? How does a stack trace help with troubleshooting your code? Let’s start with a comprehensive definition of a stack trace.

No bug sign signifying debugging stack trace

What Is a Stack Trace?

To put it simply, a stack trace represents a call stack at a certain point in time. To better understand what a call stack is, let’s dive a bit deeper into how programming languages work.

A stack is actually a data type that contains a collection of elements. The collection works as a last-in, first-out (LIFO) collection. Each element in this collection represents a function call in your code that contains logic.

Whenever a certain function call throws an error, you’ll have a collection of function calls that lead up to the call that caused the particular problem. This is due to the LIFO behavior of the collection that keeps track of underlying, previous function calls.

This also implies that a stack trace is printed top-down. The stack trace first prints the function call that caused the error and then prints the previous underlying calls that led up to the faulty call. Therefore, reading the first line of the stack trace shows you the exact function call that threw an error.

Now that you have a deeper understanding of how a stack trace works, let’s learn to read one.

Stack traces are constructed in a very similar way in most languages: they follow the LIFO stack approach. Let’s take a look at the Java stack trace below.

Exception in thread "main" java.lang.NullPointerException
        at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

The first line tells us the exact error that caused the program to print a stack trace. We see a NullPointerException, which is a common mistake for Java programmers. From the Java documentation, we note that a NullPointerException is thrown when an application attempts to use “null” in a case where an object is required.

Now we know the exact error that caused the program to exit. Next, let’s read the previous call stack. You see three lines that start with the word “at”. All three of those lines are part of the call stack.

The first line represents the function call where the error occurred. As we can see, the getTitle function of the Book class is the last executed call. Furthermore, the error occurred at line 16 in the Book class file.

The other calls represent previous calls that lead up to the getTitle function call. In other words, the code produced the following execution path:

  1. Start in main() function.
  2. Call getBookTitles() function in Author class at line 25.
  3. Call getTitle() function in Book class at line 16.

In some cases, you’ll experience chained exceptions in your stack trace. As you could have guessed, the stack trace shows you multiple exceptions that have occurred. Each individual exception is marked by the words “Caused by”.

The lowest “Caused by” statement is often the root cause, so that’s where you should look at first to understand the problem. Here’s an example stack trace that includes several “Caused by” clauses. As you can see, the root issue in this example is the database call dbCall.

Exception in thread "main" java.lang.NullPointerException at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: com.example.ServiceException: Service level error
        at com.example.myproject.Service.serviceMethod(Service.java:15)
        ... 1 more
Caused by: com.example.DatabaseException: Database level error
        at com.example.Database.dbCall(Database.java:39)
        at com.example.myproject.Service.serviceMethod(Service.java:13)
        ... 2 more

Hopefully, this information gives you a better understanding of how to approach a call stack. So, what’s a stack trace used for?

How to Use a Stack Trace

A stack trace is a valuable piece of information that can be used for debugging purposes. Whenever you encounter an error in your application, you want to be able to quickly debug the problem. Initially, developers should look in the application logs for the stack trace, because often, the stack trace tells you the exact line or function call that caused a problem.

It’s a very valuable piece of information for quickly resolving bugs because it points to the exact location where things went wrong.

In addition to telling you the exact line or function that caused a problem, a stack trace also tracks important metrics that monitor the health of your application. For example, if the average number of stack traces found in your logs increases, you know a bug has likely occurred. Furthermore, a low level of stack trace exceptions indicates that your application is in good health.

In short, stack traces and the type of errors they log can reveal various metrics related to your application as explained in the example.

Common Problems With Stack Traces and Third-Party Packages

Often, your projects will use many third-party code packages or libraries. This is a common approach for languages such as PHP or JavaScript. There is a rich ecosystem of packages maintained by the Open Source community.

In some cases, an error occurs when you send incorrect input to one of the third-party libraries you use. As you might expect, your program will print a stack trace of the function calls leading up to the problem.

However, now that you’re dealing with a third-party library, you’ll have to read many function calls before you recognize one associated with your code. In some cases, like when too many function calls happen within a third-party package, you may not even see any references to your code.

You can still try to debug your code by looking at where a particular package was used. However, if you use this particular package frequently throughout your code, debugging your application won’t be easy.

But there’s a solution to the third-party stack problem. Let’s check it out!

Solving the Third-Party Stack Trace Problem

Luckily, you can solve third-party stack trace problems by catching exceptions. A call to a third-party library may cause an exception, which will cause your program to print a stack trace containing function calls coming from within the third-party library. However, you can catch the exception in your code with the use of a try-catch statement, which is native to many programming languages such as Java or Node.js.

If you use a try-catch statement, the stack trace will start from the point where you included the try-catch logic. This presents you with a more actionable and readable stack trace that doesn’t traverse into third-party library’s function calls. It’s an effective and simple solution to make your stack traces easy to understand.

On top of that, when your program throws a Throwable instance, you can call the getStackTrace() function on the instance to access the stack trace. You can then decide to print the stack trace to your terminal so you can collect the information for log analysis. Here’s a small example where we throw an exception and manually print the stack trace.

import java.io.*; 
  
class Sum { 
  
    // Main Method 
    public static void main(String[] args) throws Exception { 
  
        try { 
            // add positive numbers 
            addPositiveNumbers(5, -5); 
        } catch (Throwable e) { 
            StackTraceElement[] stktrace = e.getStackTrace(); 
  
            // print element of stktrace 
            for (int i = 0; i < stktrace.length; i++) { 
                System.out.println("Index " + i 
                                   + " of stack trace contains = "
                                   + stktrace[i].toString()); 
            } 
        } 
    } 
  
    // method which adds two positive number 
    public static void addPositiveNumbers(int a, int b) throws Exception { 
        if (a < 0 || b < 0) {
            throw new Exception("Numbers are not Positive"); 
        } else { 
            System.out.println(a + b); 
        } 
    } 
}

The printed output will look like this.

Index 0 of stack trace contains = Sum.addPositiveNumbers(File.java:26) Index 1 of stack trace contains = Sum.main(File.java:6)

Next, let’s learn how log management and stack traces work together.

Log Management and Stack Traces

You might wonder what log management and stack traces have to do with each other, and actually, they’re very compatible.

It’s best practice for your DevOps team to implement a logging solution. Without an active logging solution, it’s much harder to read and search for stack traces. A logging solution provides you with an easy-to-use interface and better filtering capabilities.

A log management solution helps aggregate logs, index them, and make them searchable, all from a single interface. You can run advanced queries to find specific logs or stack trace information. This approach is much faster than using the CTRL+F key combination to look through your logs.

Conclusion

To summarize, we focused on the need for a logging solution to access stack traces and any other relevant information your application outputs. A stack trace is one of the most valuable pieces of information to help developers identify problems quickly.

Furthermore, a stack trace shows an exact execution path, providing context to developers trying to solve bugs. The first line in the call stack represents the last executed function call, so remember to always read a stack trace top-down. That first function call is responsible for throwing an exception.

Want to decrease your bug resolution time? Check out Scalyr’s log management solution. If you want to learn more about log formatting and best practices for logging, check out this log formatting article.

Понравилась статья? Поделить с друзьями:
  • Stack trace ошибка в сталкере anomaly
  • Stack overflow ошибка как исправить
  • Stability track опель астра ошибка
  • Stabilitrak ошибка cadillac cts 2
  • Ssd диск ошибка при загрузке