Skip to content

Strange Java Stack Frames: lambda$0, $1, init and Proxies

A lambda$main$0 stack trace, access$000, <init>, $Proxy0: what odd frame names in a Java stack trace mean, where javac puts them, and how to read past them.

stack-traces jvm-internals
Gopi Gorantala
Reading Progress

On This Page

Line 7 of a file where nothing happens on line 7

OpenJDK 21.0.12.1, Linux, a small rule evaluator. I compiled it with --release 8 (more on why in a minute) and ran it with plain java -cp out8 RuleLab inner from the console. This came out, copied as-is:

java.lang.IllegalStateException: secret A-17
	at RuleLab.secret(RuleLab.java:40)
	at RuleLab.access$000(RuleLab.java:7)
	at RuleLab$Peeker.peek(RuleLab.java:45)
	at RuleLab.viaInner(RuleLab.java:73)
	at RuleLab.main(RuleLab.java:95)

Line 7 of RuleLab.java is public class RuleLab {. Nothing executes on line 7. There's also a method called access$000 that I never wrote, and it's sitting in the middle of my own call chain.

Here's a second one, from the same program, a lambda this time. If you've pasted a lambda$main$0 stack trace into a search box, this is the family you're in:

java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab.lambda$viaLambda$0(RuleLab.java:50)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)
	at RuleLab.viaLambda(RuleLab.java:51)
	at RuleLab.main(RuleLab.java:91)

lambda$viaLambda$0 isn't in the source either. It is yours, though, and access$000 isn't. Telling those two apart on sight is most of this article. The rest of it is $1, <init>, <clinit>, doubled frames and $Proxy0. If you haven't read the hub post, how to read a Java stack trace covers the frame format I'm assuming here.

The short read

Every odd name in a trace comes from one of three places: javac generated a method or class for you, the JVM named a special method (<init>, <clinit>), or the runtime generated a whole class at run time (a proxy). None of them is a bug. Each one tells you something about how the call got from A to B.

My first instinct on the doubled check frame you'll meet below was recursion, so I went looking for a method calling itself. There isn't one. On access$000(RuleLab.java:7) I assumed a broken line table. Also wrong; I'll show where line 7 comes from.

Which frame would I open? In the lambda trace, lambda$viaLambda$0 at line 50, because that's the body you wrote. In the access$000 trace, I'd skip it and open secret at line 40, or peek at line 45.

Make this trace yourself

Pinned to OpenJDK 21.0.12.1 (the Ubuntu 24.04 package). Any JDK 21 should print the same shape. I did not run JDK 8, 17 or 25 runtimes, so every trace in here is JDK 21 output.

Save this as RuleLab.java. Keep the blank lines and the line breaks. Every line number in the traces depends on them. It's written to compile with --release 8 as well, which is why there's no record and no arrow-switch in it.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class RuleLab {
    interface Rule<T> {
        void check(T order) throws Exception;
    }

    static class Order {
        final String id; final int qty;
        Order(String id, int qty) { this.id = id; this.qty = qty; }
        String id() { return id; }
        void validate() {
            if (qty <= 0) throw new IllegalStateException("qty " + qty + " on " + id);
        }
    }

    static class QtyRule implements Rule<Order> {
        public void check(Order o) {
            o.validate();
        }
    }

    static class Threshold {
        final int limit;
        Threshold(int limit) {
            if (limit < 0) throw new IllegalArgumentException("limit " + limit);
            this.limit = limit;
        }
    }

    static class Defaults {
        static final int MAX = Integer.parseInt(System.getProperty("max", "x12"));
    }

    private String secret(Order o) {
        throw new IllegalStateException("secret " + o.id());
    }

    class Peeker {
        String peek(Order o) {
            return secret(o);
        }
    }

    static void viaLambda(List<Order> orders) {
        Consumer<Order> c = o -> o.validate();
        orders.forEach(c);
    }

    static void viaMethodRef(List<Order> orders) {
        orders.forEach(Order::validate);
    }

    static void viaAnon(Order o) throws Exception {
        Rule<Order> r = new Rule<Order>() {
            public void check(Order x) {
                x.validate();
            }
        };
        r.check(o);
    }

    static void viaBridge(Order o) throws Exception {
        Rule raw = new QtyRule();
        raw.check(o);
    }

    static void viaInner(Order o) {
        new RuleLab().new Peeker().peek(o);
    }

    static void viaProxy(Order o) throws Exception {
        InvocationHandler h = (proxy, m, a) -> {
            new QtyRule().check((Order) a[0]);
            return null;
        };
        @SuppressWarnings("unchecked")
        Rule<Order> r = (Rule<Order>) Proxy.newProxyInstance(
            RuleLab.class.getClassLoader(), new Class<?>[] { Rule.class }, h);
        r.check(o);
    }

    public static void main(String[] args) throws Exception {
        Order bad = new Order("A-17", 0);
        try {
            String w = args[0];
            if (w.equals("lambda")) viaLambda(Arrays.asList(bad));
            else if (w.equals("mref")) viaMethodRef(Arrays.asList(bad));
            else if (w.equals("anon")) viaAnon(bad);
            else if (w.equals("bridge")) viaBridge(bad);
            else if (w.equals("inner")) viaInner(bad);
            else if (w.equals("proxy")) viaProxy(bad);
            else if (w.equals("init")) new Threshold(-1);
            else if (w.equals("clinit")) System.out.println(Defaults.MAX);
            else throw new IllegalArgumentException(w);
        } catch (Throwable t) {
            t.printStackTrace(System.out);
        }
    }
}

Steps:

  1. Compile normally: javac -d out21 RuleLab.java. You'll get a note about unchecked operations. That's the raw Rule in viaBridge, on purpose.
  2. Run the lambda one: java -cp out21 RuleLab lambda. You should get exactly the second trace above. Line numbers match if you copied the file exactly.
  3. Run the rest: lambda, mref, anon, bridge, inner, proxy, init, clinit. I print each below, annotated.
  4. Compile for an old target: javac --release 8 -Xlint:-options -d out8 RuleLab.java, then java -cp out8 RuleLab inner. That's the first trace in the article. This is JDK 21 running version-52 class files. It is not a JDK 8 runtime, and I want to be clear about that.
  5. Show the frames the JVM hides: java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -cp out21 RuleLab lambda.

Near-miss: if inner prints no access$000 frame, you compiled without --release 8. That's the point of the next sections, not a mistake.

A second file covers the names the first one doesn't. Same rules, keep the line layout (the comments say which line matters):

import java.util.function.Supplier;

public class Names {
    static final Supplier<String> S = () -> boom("static");          // line 4
    final Supplier<String> f = () -> boom("field");                  // line 5

    enum Mode {
        FAST { String run() { return boom("enum"); } },              // line 8
        SLOW { String run() { return "ok"; } };
        abstract String run();
    }

    static String boom(String where) {
        throw new IllegalStateException(where);                      // line 14
    }

    static String nested() {
        Supplier<Supplier<String>> outer = () -> () -> boom("nested"); // line 18
        return outer.get().get();
    }

    static String local() {
        class Local { String go() { return boom("local"); } }        // line 23
        return new Local().go();
    }

    static void show(Runnable r) {
        try { r.run(); } catch (Throwable t) { t.printStackTrace(System.out); }
    }

    public static void main(String[] args) {
        show(() -> S.get());
        show(() -> new Names().f.get());
        show(() -> Mode.FAST.run());
        show(() -> nested());
        show(() -> local());
    }
}

Compile it with javac -d outN Names.java, run java -cp outN Names. And a small proxy file, P.java:

import java.lang.reflect.*;
public class P {
    public static void main(String[] a) {
        Runnable r = (Runnable) Proxy.newProxyInstance(P.class.getClassLoader(), new Class<?>[]{Runnable.class},
            (p, m, x) -> { throw new IllegalStateException("from handler"); });
        try { r.run(); } catch (Throwable t) { t.printStackTrace(System.out); }
        Callable2 c = (Callable2) Proxy.newProxyInstance(P.class.getClassLoader(), new Class<?>[]{Callable2.class},
            (p, m, x) -> { throw new java.io.IOException("checked"); });
        try { c.go(); } catch (Throwable t) { t.printStackTrace(System.out); }
    }
    public interface Callable2 { void go(); }
}

javac -d outP P.java && java -cp outP P. IDE consoles tend to fold the JDK frames, so run these from a terminal the first time.

lambda$main$0 and friends: every odd frame, decoded

Lambdas and method references

java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)                  (1)
	at RuleLab.lambda$viaLambda$0(RuleLab.java:50)              (2)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)  (3)
	at RuleLab.viaLambda(RuleLab.java:51)                       (4)
	at RuleLab.main(RuleLab.java:91)

(1) is where the exception was created, your real code. (2) is the lambda body: javac turned o -> o.validate() into a private static method named lambda$ + the enclosing method + $ + a number. The line is the line inside the lambda. (3) is the JDK calling your lambda, on the java.base module. (4) is the method that created the lambda and handed it to forEach, and line 51 is the forEach call.

Notice what's missing between (2) and (3): the object that implements Consumer. It exists, and I'll get to it. The JVM hides it.

Now the method reference. Here it is, same job, orders.forEach(Order::validate):

java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at java.base/java.util.Arrays$ArrayList.forEach(Arrays.java:4305)
	at RuleLab.viaMethodRef(RuleLab.java:55)
	at RuleLab.main(RuleLab.java:92)

No lambda$ frame at all. A method reference to an existing method doesn't need a synthetic body, so the trace is one frame shorter and has no generated name in it.

The number after the second dollar sign

That trailing 0 surprised me more than anything else in this lab. From Names.java:

java.lang.IllegalStateException: static
	at Names.boom(Names.java:14)
	at Names.lambda$static$0(Names.java:4)
	at Names.lambda$main$4(Names.java:32)
	at Names.show(Names.java:28)
	at Names.main(Names.java:32)

The lambda in main is lambda$main$4. Not $0. The counter is per class, not per method, and main has five lambdas that come after the ones above it. The rest of the run, with the boom frame on top and everything below the interesting frame (lambda$main$N, show, main) cut by me:

	at Names.lambda$new$1(Names.java:5)        <- lambda in an instance field initializer
	at Names$Mode$1.run(Names.java:8)          <- enum constant with a body
	at Names.lambda$nested$2(Names.java:18)    <- the INNER of () -> () -> boom("nested")
	at Names$1Local.go(Names.java:23)          <- a local class

The field initializer's lambda is named after the constructor (new), a static one after static. The inner lambda of a nested pair got $2 and the outer $3 (I checked with javap -p), so inner before outer. And I added one throwaway lambda to line 4 of a scratch copy, on the same line so nothing else moved. Every later name shifted by one: lambda$static$1, lambda$new$2, lambda$main$5. Same lines, different names.

Anonymous, inner, local, and the doubled frame

java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab$1.check(RuleLab.java:61)                          (1)
	at RuleLab$1.check(RuleLab.java:59)                          (2)
	at RuleLab.viaAnon(RuleLab.java:64)
	at RuleLab.main(RuleLab.java:93)

RuleLab$1 is the first anonymous class in RuleLab; the number is again assigned by javac, in order. Names$1Local above is the same scheme for a local class (the digit comes before the name), and Names$Mode$1 is an enum constant with a body.

And now the doubled check. (1) is your method, line 61. (2) is a bridge method: the interface says check(Object) after erasure, your class implements check(Order), so javac generates a check(Object) that casts and calls the real one. The frame points at line 59, the line where the class starts. Same story for QtyRule in the bridge run, lines 23 and 21. I confirmed with javap -p -v that the generated method has ACC_BRIDGE, ACC_SYNTHETIC and a checkcast followed by the real call.

access$000, and the line 7 mystery

With --release 8 classes, Peeker (an inner class) calls a private method of RuleLab. Before Java 11 the JVM had no way to allow that, so javac generated a package-private static method on the outer class, access$000(RuleLab, Order), and routed the call through it. javap shows it as static and ACC_SYNTHETIC. Its line number is the line of the outer class declaration: 7. That's your answer to line 7. With the default javac on 21 the frame is gone, because nested classes became nestmates in Java 11 (JEP 181) and javap -v on the compiled classes shows the NestHost and NestMembers attributes instead.

<init> and <clinit>

java.lang.IllegalArgumentException: limit -1
	at RuleLab$Threshold.<init>(RuleLab.java:30)
	at RuleLab.main(RuleLab.java:97)

<init> is a constructor, and instance field initializers and instance initializer blocks get compiled into it too. So an exception from a field initializer shows <init> with the field's line. The angle brackets are there because the JVM needs method names that can't collide with anything you can write in Java.

java.lang.ExceptionInInitializerError
	at RuleLab.main(RuleLab.java:98)
Caused by: java.lang.NumberFormatException: For input string: "x12"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at java.base/java.lang.Integer.parseInt(Integer.java:778)
	at RuleLab$Defaults.<clinit>(RuleLab.java:36)
	... 1 more

<clinit> is the static initializer: static field initializers and static {} blocks. The outer ExceptionInInitializerError only has the frame that triggered the class load, main at line 98. The frame you want, <clinit> at line 36, is down in the Caused by. The elision rule from the Caused by article applies unchanged: that ... 1 more is the main frame, shared with the outer trace.

The proxy

java.lang.IllegalStateException: qty 0 on A-17
	at RuleLab$Order.validate(RuleLab.java:17)
	at RuleLab$QtyRule.check(RuleLab.java:23)
	at RuleLab.lambda$viaProxy$1(RuleLab.java:78)               (1)
	at $Proxy0.check(Unknown Source)                            (2)
	at RuleLab.viaProxy(RuleLab.java:84)
	at RuleLab.main(RuleLab.java:96)

(2) is a class the JDK generated at run time. It implements Rule, and each method forwards to your InvocationHandler. (1) is the handler, which in my lab happens to be a lambda, so you get a lambda$ name numbered $1 because viaLambda already took $0. With -XX:+ShowHiddenFrames the lambda's generated class shows up too:

	at RuleLab.lambda$viaProxy$1(RuleLab.java:78)
	at RuleLab$$Lambda/0x00007f4a5c000400.invoke(Unknown Source)
	at $Proxy0.check(Unknown Source)

The lambda trace from the same flag has RuleLab$$Lambda/0x00007f0b74001800.accept(Unknown Source) between your lambda and forEach. Different hex on every run, so don't match on it.

P.java shows the two other faces of a proxy. A proxy for Runnable prints like this:

java.lang.IllegalStateException: from handler
	at P.lambda$main$0(P.java:5)
	at jdk.proxy1/jdk.proxy1.$Proxy0.run(Unknown Source)
	at P.main(P.java:6)

And when the handler throws a checked exception the interface doesn't declare, the proxy wraps it:

java.lang.reflect.UndeclaredThrowableException
	at jdk.proxy1/jdk.proxy1.$Proxy1.go(Unknown Source)
	at P.main(P.java:9)
Caused by: java.io.IOException: checked
	at P.lambda$main$1(P.java:8)
	... 2 more

Root cause is the Caused by, same as always. If you've ever stared at an InvocationTargetException with a null message, that one is the reflection cousin.

Who names these frames: javac, the JVM, or Proxy

This took me well over a day of rebuilding the lab and re-reading Javadoc before it sorted itself out, and what finally organised it was one question per name: who picked this? It's one of three, and they behave differently.

javac picks lambda$...$N, Outer$1, Outer$1Local, Outer$Mode$1, bridge methods and access$NNN. All of these are synthetic. They exist in the class file with the synthetic flag set, they show up in traces because the JVM doesn't treat synthetic methods specially, and their names aren't a stable interface. The N is a class-wide counter. I didn't read javac's source for the exact assignment order; what I can tell you is what I observed above (member order, inner lambda before outer). That's enough to say don't build anything on it.

Line numbers on generated methods were the slow part. A bridge method has no source of its own, so its line table points at the declaration, and access$000 does the same. The lambda body is different: it is your code, so its lines are real.

The JVM picks <init> and <clinit>. These are fixed names from the class file format. Nothing to configure.

The runtime picks $ProxyN and, with the right flag, RuleLab$$Lambda/0x.... The Proxy Javadoc is blunt about this: "The unqualified name of a proxy class is unspecified. The space of class names that begin with the string "$Proxy" should be, however, reserved for proxy classes." Package and module are unspecified too. In my lab, a proxy for a non-public interface (my nested Rule, no public) landed in the interface's own package, so the frame prints as bare $Proxy0. A proxy for a public interface in an exported package landed in jdk.proxy1. The Javadoc describes exactly that split. It also means grepping logs for jdk.proxy1 is a habit I wouldn't build.

Why Unknown Source on the proxy frame? I saved the generated classes with -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true and ran javap -v on them. Neither SourceFile nor LineNumberTable is there. It's not your build. There's nothing to fix. (If you want the full story on those two attributes, it's in the Unknown Source article.)

The lambda classes are the part with a history. The generated class that implements Consumer is a hidden class, and JEP 371 (JDK 15) says it plainly: "Methods of hidden classes are not shown in stack traces by default." The way to see them is the diagnostic flag above. StackWalker has an equivalent switch, SHOW_HIDDEN_FRAMES, which the Javadoc describes as a superset of SHOW_REFLECT_FRAMES; I'll use it properly in the StackWalker article. People wanted these frames gone long before 15: JDK-8016334, filed in 2013 and closed as a duplicate, complains about a StackTraceTest$$Lambda$1.run(Unknown Source) frame making traces longer. I haven't run anything before 21, so I can't tell you which release in between started hiding them. Treat my version claims as "21 does this, JEP 371 says 15".

The frame everyone blames

Four misreadings I've watched people (me included) make.

"The doubled frame is recursion." Same class, same method name, two consecutive frames, different line numbers. One of those lines is the class declaration or the new Foo() { line. That's a bridge. Real recursion repeats the same line, over and over.

"The access$000 frame is where it broke." It's a trampoline. Look at the frame above it for what actually threw, and the frame below for who called.

"Unknown Source on $Proxy0 means someone built without -g." No. Proxies never had the attributes.

"I'll alert on lambda$main$4." Don't. Add one lambda above it in the class and it's lambda$main$5. Your error grouping splits in two the day you deploy an unrelated change. It took me three rebuilds of the lab to believe the counter was class-wide.

And the hub's rule still holds: the top frame is where the Throwable was constructed, which for a lambda is often inside the JDK or deeper, not the lambda.

What to change, and what it won't fix

Name the thing. If a lambda body is more than a line, pull it into a named method and call that. The trace then has a real name and a stable one:

orders.forEach(Order::validate);   // no synthetic frame at all, see the mref trace above
orders.forEach(this::checkOne);    // frame is RuleLab.checkOne, not lambda$viaLambda$0

The cost is zero at runtime and one extra method in the class. The lambdas and streams article goes further, with a stream pipeline where this matters a lot more.

For proxies, use a named InvocationHandler class instead of a lambda, so the handler shows up as a frame with a real name. I'd expect that to read better, but I didn't run it this time, so check it against your own trace.

What doesn't help: running production with -XX:+ShowHiddenFrames. It's a diagnostic flag, it makes every lambda trace longer, and the 0x00007f... addresses change per run, so you break whatever grouping you had. Use it on your laptop while you learn what the frames mean.

Compile with debug info on (see the Unknown Source article). Not for $Proxy0, but because the other frames in these traces need line numbers.

Keeping it from biting again

When you group errors in a log pipeline, group on exception type plus the first frame in your own package that isn't synthetic. Strip lambda$...$N numbers and $N suffixes from the fingerprint, or keep the method name and drop the counter.

Version honesty, since a trace from a different JDK will look different from mine. Everything above is from OpenJDK 21.0.12.1. access$000 appears only when classes are compiled for a target older than 11; I produced it with --release 8 on JDK 21 and didn't run a JDK 8 runtime. Hidden lambda frames are off by default as of JEP 371 (JDK 15). Proxy package and module names are unspecified, so they can differ between JDKs. I did not run JDK 17 or 25, and I did not run CGLIB or ByteBuddy proxies at all. Framework-generated classes with $$Enhancer... style names follow the same logic, but check your own trace; the framework-noise article is where I'll cover those.

What to remember

  • lambda$method$N is your lambda body. Read it. N is a per-class counter and changes when you add a lambda above it.
  • A method reference leaves no synthetic frame. A lambda leaves one.
  • The same method twice in a row with different lines is a bridge, not recursion.
  • access$000 means a pre-11 target and a private access across nested classes. Skip it, and ignore its line number.
  • <init> covers constructors and field initializers; <clinit> is static init, and its frame sits in the Caused by of an ExceptionInInitializerError.
  • $Proxy0 with Unknown Source is normal, and its package name is unspecified.

Things people paste into search when they hit these

What does lambda$main$0 mean in a Java stack trace?
The method javac generated from a lambda body in main. The 0 is a per-class counter, so it changes when you add or move a lambda. The line number inside it is real. Observed on JDK 21.

What is access$000 in a Java stack trace?
A synthetic accessor javac generates so an inner class can call a private member of the outer class. You see it when classes target something older than Java 11; since JEP 181 nested classes are nestmates and the frame disappears. Its line is the outer class declaration.

Why does the same method appear twice in my stack trace?
If the two lines differ and one is a class declaration or an anonymous-class line, the second frame is a bridge method from generics. If the same line repeats many times, that's actual recursion, and you're heading toward a StackOverflowError.

What do <init> and <clinit> mean in a stack trace?
<init> is a constructor (plus instance field initializers and initializer blocks). <clinit> is the class's static initializer. A failure in <clinit> is wrapped in ExceptionInInitializerError, and the real frames are in its Caused by.

stack-tracesjvm-internals

Staff engineer architecting Kafka and Flink platforms for banking, where a dropped event is a reconciliation break, not a retry. Through javahandbook.com, I share production-tested guidance on building reliable event-driven systems.

Comments