Skip to content

Instantly share code, notes, and snippets.

@simonis
Last active June 30, 2023 14:31
Show Gist options
  • Save simonis/084125aa0545118ac3e836fe24eab5f3 to your computer and use it in GitHub Desktop.
Save simonis/084125aa0545118ac3e836fe24eab5f3 to your computer and use it in GitHub Desktop.
Get the source code line number in Java programmatically at runtime

Get the source code line number
in Java programmatically at runtime

A quick comparison of various methods to get line number information in Java. Compiles and runs with Java 8+.

For Java 8 we compare the public Throwable::getStackTrace()[2] with the private Throwable::getStackTraceElement(2) method to get the line number information from the third, top-most frame for various stack depth.

Java 8

<JAVA-8_HOME>/bin/java -XX:+UseSerialGC -Xbatch -XX:-TieredCompilation -XX:-UseOnStackReplacement GetLineNumber
Throwable::getStackTrace()[2] = GetLineNumber.recursive(GetLineNumber.java:102)
  0 :   2063ns
  8 :   4791ns
 16 :   7611ns
 32 :  13340ns
 64 :  24638ns
128 :  50544ns
256 :  96262ns
512 : 188946ns
Throwable::getStackTraceElement(2) [reflective] = GetLineNumber.recursive(GetLineNumber.java:102)
  0 :   1045ns
  8 :   1052ns
 16 :   1296ns
 32 :   1984ns
 64 :   3167ns
128 :   5469ns
256 :   9719ns
512 :  18108ns

As you can see, the performance of both versions depends on the stack depth but the private method performs much better. That's because while it still acquires a full stack trace, it only allocates the a single StackTraceElement whereas the public Throwable::getStackTrace() method allocates StackTraceElement for all stack frames although we're only interested in a single frame in this example.

Java 9+

Java 9 redesigned the stack walking code and added a new Stack walking API. During this re-implementation (see JDK-8150778), the private Throwable::getStackTraceElement() method was removed in favour of the new Throwable::getStackTraceElements() method which does a single down-call to the VM to fetch all StackTraceElements at once.

<JAVA-11_HOME>/bin/java -XX:+UseSerialGC -Xbatch -XX:-TieredCompilation -XX:-UseOnStackReplacement GetLineNumber
Throwable::getStackTrace()[2] = GetLineNumber.recursive(GetLineNumber.java:102)
  0 :   1392ns
  8 :   3056ns
 16 :   4510ns
 32 :   7723ns
 64 :  13989ns
128 :  26340ns
256 :  52131ns
512 : 102076ns
StackWalker::walk(s -> s.limit(3).skip(2)) [reflective] = GetLineNumber.recursive(GetLineNumber.java:102)
  0 :   1841ns
  8 :   1405ns
 16 :   1393ns
 32 :   1401ns
 64 :   1377ns
128 :   1531ns
256 :   1419ns
512 :   1432ns

As you can see, the new StackWalker API doesn't depend on the absolute stack depth any more if we're only interested in some of the top frames. The other nice thing is that the new implementation also improves the performance of the old Throwable::getStackTrace()[2] method (mostly because of "JDK-8216302: StackTraceElement::fill_in can use cached Class.name" which was integrated in JDK 13 and backported to 11.0.3).

Notice however that the estimateDepth argument which we pass to StackWalker.getInstance() isn't directly honoured. Instead the current implementation enforces a minimum of MIN_BATCH_SIZE (equals to 8) frames on each down-call to the VM (of which the first two entries are reserved for internal bookkeeping).

Log4j2

Out of interest I've checked how the popular Log4j2 logging framework is computing line number information. The corresponding StackLocator::calcLocation() method is in the log4j-api.jar library which is a multi-release jar file and contains two versions of the StackLocator class, one for JDK 8 and a second one for JDK 9+. The old version uses Throwable::getStackTrace() while the new one is using StackWalker::walk().

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Stream;
public class GetLineNumber {
private static int[] stackDepth = { 0, 8, 16, 32, 64, 128, 256, 512 };
private static int WARMUP = Integer.getInteger("warmup", 200);;
private static int ITERATIONS = Integer.getInteger("iterations", 100_000);
static Object bh;
//
// Set up reflective access to Throwable::getStackTraceElement
// This private method was removed in Jave 9.
//
private static Method gste;
static {
try {
gste = Throwable.class.getDeclaredMethod("getStackTraceElement", int.class);
gste.setAccessible(true);
} catch (Exception ignore) {
// Only available in JDK 8
gste = null;
}
}
//
// Set up reflective access to StackWalker::walk(s -> s.limit(3).skip(2).findFirst())
// StackWalker was added in Java 9, but this code can be compiled with Java 8.
//
private static Optional filterFunction(Stream st) {
return st.limit(3).skip(2).findFirst();
}
private static Function<Stream, Optional> filter = GetLineNumber::filterFunction;
private static Method walk;
private static Object walker;
static {
try {
Class stackWalker = Class.forName("java.lang.StackWalker");
Class stackWalkerOption = Class.forName("java.lang.StackWalker$Option");
Method getInstance = stackWalker.getDeclaredMethod("getInstance", Set.class, int.class);
walk = stackWalker.getDeclaredMethod("walk", Function.class);
walker = getInstance.invoke(null, Collections.emptySet(), 3);
} catch (Exception ignore) {
// Only available in JDK 9+
walk = null;
walker = null;
}
}
interface Benchmark {
long execute(int iterations);
}
public static long getStackTraceElement(int iterations) {
try {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = (StackTraceElement) gste.invoke(new Throwable(), 2);
}
return System.nanoTime() - start;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
}
return -1;
}
public static long getStackTrace(int iterations) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = new Throwable().getStackTrace()[2];
}
return System.nanoTime() - start;
}
public static long getStackFrameReflective(int iterations) {
try {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = ((Optional) walk.invoke(walker, filter)).get();
}
return System.nanoTime() - start;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
}
return -1;
}
public static long getLineNumber(int iterations, Benchmark b) {
System.gc();
return b.execute(iterations);
}
public static long recursive(int depth, int iterations, Benchmark b) {
if (depth > 0) {
return recursive(depth - 1, iterations, b);
} else {
return getLineNumber(iterations, b);
}
}
public static void main(String[] args) {
System.out.print("Throwable::getStackTrace()[2] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber::getStackTrace);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber::getStackTrace);
System.out.println(String.format("%3d : %6dns", depth, (time/ITERATIONS)));
}
// Java 8
if (gste != null) {
System.out.print("Throwable::getStackTraceElement(2) [reflective] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber::getStackTraceElement);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber::getStackTraceElement);
System.out.println(String.format("%3d : %6dns", depth, (time/ITERATIONS)));
}
}
// Java 9+
if (walker != null) {
System.out.print("StackWalker::walk(s -> s.limit(3).skip(2)) [reflective, estimateDepth=3] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber::getStackFrameReflective);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber::getStackFrameReflective);
System.out.println(String.format("%3d : %6dns", depth, (time / ITERATIONS)));
}
}
}
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
public class GetLineNumber9 {
private static int[] stackDepth = { 0, 8, 16, 32, 64, 128, 256, 512 };
private static int WARMUP = Integer.getInteger("warmup", 200);;
private static int ITERATIONS = Integer.getInteger("iterations", 100_000);
static Object bh;
//
// Set up reflective access to StackWalker::walk(s -> s.limit(3).skip(2).findFirst())
// StackWalker was added in Java 9, but this code can be compiled with Java 8.
//
private static Optional filterFunction(Stream st) {
return st.limit(3).skip(2).findFirst();
}
private static Function<Stream, Optional> filter = GetLineNumber9::filterFunction;
private static Method walk;
private static Object walker;
static {
try {
Class stackWalker = Class.forName("java.lang.StackWalker");
Class stackWalkerOption = Class.forName("java.lang.StackWalker$Option");
Method getInstance = stackWalker.getDeclaredMethod("getInstance", Set.class, int.class);
walk = stackWalker.getDeclaredMethod("walk", Function.class);
walker = getInstance.invoke(null, Collections.emptySet(), 3);
} catch (Exception ignore) {
// Only available in JDK 9+
walk = null;
walker = null;
}
}
interface Benchmark {
long execute(int iterations);
}
public static long getStackTrace(int iterations) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = new Throwable().getStackTrace()[2];
}
return System.nanoTime() - start;
}
public static long getStackFrameReflective(int iterations) {
try {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = ((Optional) walk.invoke(walker, filter)).get();
}
return System.nanoTime() - start;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
}
return -1;
}
private static final StackWalker WALKER = StackWalker.getInstance(Collections.emptySet(), 3);
public static long getStackFrame(int iterations) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = WALKER.walk(s -> s.limit(3).skip(2).findFirst()).get();
}
return System.nanoTime() - start;
}
private static final StackWalker WALKER2 = StackWalker.getInstance(Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE), 3);
public static long getStackFrame2(int iterations) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = WALKER2.walk(s -> s.limit(3).skip(2).findFirst()).get();
}
return System.nanoTime() - start;
}
private static final StackWalker WALKER3 = StackWalker.getInstance();
public static long getStackFrame3(int iterations) {
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
bh = WALKER3.walk(s -> s.limit(3).skip(2).findFirst()).get();
}
return System.nanoTime() - start;
}
public static long getLineNumber(int iterations, Benchmark b) {
System.gc();
return b.execute(iterations);
}
public static long recursive(int depth, int iterations, Benchmark b) {
if (depth > 0) {
return recursive(depth - 1, iterations, b);
} else {
return getLineNumber(iterations, b);
}
}
public static void main(String[] args) {
System.out.print("Throwable::getStackTrace()[2] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber9::getStackTrace);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber9::getStackTrace);
System.out.println(String.format("%3d : %6dns", depth, (time/ITERATIONS)));
}
System.out.print("StackWalker::walk(s -> s.limit(3).skip(2)) [reflective, estimateDepth=3] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber9::getStackFrameReflective);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber9::getStackFrameReflective);
System.out.println(String.format("%3d : %6dns", depth, (time / ITERATIONS)));
}
System.out.print("StackWalker::walk(s -> s.limit(3).skip(2)) [estimateDepth=3] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber9::getStackFrame);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber9::getStackFrame);
System.out.println(String.format("%3d : %6dns", depth, (time / ITERATIONS)));
}
System.out.print("StackWalker::walk(s -> s.limit(3).skip(2)) [RETAIN_CLASS_REFERENCE, estimateDepth=3] = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber9::getStackFrame2);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber9::getStackFrame2);
System.out.println(String.format("%3d : %6dns", depth, (time / ITERATIONS)));
}
System.out.print("StackWalker::walk(s -> s.limit(3).skip(2)) = ");
for (int i = 0; i < WARMUP; i++) {
recursive(stackDepth[stackDepth.length - 1], WARMUP, GetLineNumber9::getStackFrame3);
}
System.out.println(bh);
for (int depth : stackDepth) {
long time = recursive(depth, ITERATIONS, GetLineNumber9::getStackFrame3);
System.out.println(String.format("%3d : %6dns", depth, (time / ITERATIONS)));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment