Exactly what bytecode does the compiler emit for a @JvmStatic companion function, and where does the actual implementation live?
answer
- Body stays on Companion; static is an added forwarder
- Named object forwards via INSTANCE field
- Property → static getter/setter forwarders
- Two distinct Method objects in reflection
- Not valid on top-level or const val
basics
~10 sThe compiler keeps the method on the Companion object and adds a second static method on the outer class. That static method just forwards the call to the Companion singleton's instance method.
solid answer
~40 s@JvmStatic does not move the implementation onto the outer class. The compiler still emits the member as an instance method on `Foo$Companion`, where the real body lives. It then ADDITIONALLY generates a `static` method of the same name and signature on the outer class `Foo` that delegates to `Foo.Companion.bar()` (it loads the `Companion` singleton field and invokes the instance method). So both call forms — `Foo.bar()` and `Foo.Companion.bar()` — resolve to the same logic. For a `@JvmStatic` property the compiler generates static getter/setter methods on the outer class that forward to the companion's accessors. For a member of a named `object`, the static method delegates to the object's `INSTANCE` field. The duplication is invisible to Kotlin but matters for reflection and for understanding stack traces.
code
kotlin · 5 linesobject Registry {
private val map = HashMap<String, Int>()
@JvmStatic fun put(k: String, v: Int) { map[k] = v } // Java: Registry.put("a", 1)
// forwarder on Registry delegates to Registry.INSTANCE.put(...)
}go deeper
Knows a static method appears on the outer class but may not know the body stays on Companion.
Correctly states the implementation stays on Companion/INSTANCE and the outer static is a forwarder, including for properties.
Reasons about reflection seeing two Method objects, stack-trace shape, and the const val restriction.
Connects forwarder semantics to ABI/binary-compatibility and reflection-based frameworks relying on either method.
## Where the implementation lives A common misconception is that `@JvmStatic` *relocates* the function to the outer class. It does not. The **real body stays on the companion's class** (`Foo$Companion`) as an instance method. `@JvmStatic` makes the compiler emit an **additional** synthetic-bridge-like static method on the outer class that forwards. ```kotlin class Foo { companion object { @JvmStatic fun bar(): Int = 42 } } ``` Conceptually the generated Java equivalent is: ```java public final class Foo { public static final Companion Companion = new Companion(); public static final class Companion { public final int bar() { return 42; } // real body here } public static int bar() { return Companion.bar(); } // generated forwarder } ``` ## Named object case For a `@JvmStatic` member of `object Service { ... }`, the singleton is stored in the static field `Service.INSTANCE`. The generated static method on `Service` forwards to `INSTANCE.method()`. ## Properties For a `@JvmStatic val`/`var` in a companion, the compiler generates static accessor methods (`getX()` / `setX()`) on the outer class that forward to the companion's accessors. Note this is **different from `@JvmField`**, which instead exposes the backing field directly as a public static field with no accessors. ## Why the forwarder matters - **Reflection**: `Foo::class.java.getMethod("bar")` finds the static forwarder; `Foo.Companion::class.java.getMethod("bar")` finds the instance method. They are two distinct `Method` objects. - **Stack traces / debugging**: a call through `Foo.bar()` will show the forwarder, then the companion method. - **`open`/inheritance**: the companion's instance method is the polymorphic one; the static forwarder is not virtual. ## Restrictions - `@JvmStatic` is only valid on members of a companion object or a named object — not on top-level functions (already static) and not on regular class members. - It cannot be applied to `const val` (those are already inlined static fields) — there it is redundant/disallowed.
- If you call Foo.bar() from Java and it throws, what do you see in the stack trace?Typically two frames: the static forwarder on Foo and the actual instance method on Foo$Companion where the body lives.
- Why can't @JvmStatic apply to const val?A const val is already emitted as a static final field with a compile-time constant, so there is no method to make static — the annotation is redundant and rejected.
saying these in an interview costs you the question
- Claiming the body moves onto the outer class
- Saying the Companion instance method disappears
- Confusing the forwarder semantics with @JvmField field exposure
- Asserting @JvmStatic works on any class member
- Thinking reflection sees only one method