skip to content

Exactly what bytecode does the compiler emit for a @JvmStatic companion function, and where does the actual implementation live?

level: middleimportance: should knowfreq 50%

answer

  1. Body stays on Companion; static is an added forwarder
  2. Named object forwards via INSTANCE field
  3. Property → static getter/setter forwarders
  4. Two distinct Method objects in reflection
  5. Not valid on top-level or const val

basics

~10 s

The 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 lines
kotlin
object 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

for a junior

Knows a static method appears on the outer class but may not know the body stays on Companion.

for a middle

Correctly states the implementation stays on Companion/INSTANCE and the outer static is a forwarder, including for properties.

for a senior

Reasons about reflection seeing two Method objects, stack-trace shape, and the const val restriction.

for a principal

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

context