skip to content

How do Kotlin's visibility modifiers map onto JVM bytecode access flags, and why does that matter?

level: principalimportance: nice to knowfreq 30%

answer

  1. JVM has no internal; Kotlin has no package-private
  2. internal → public + mangled name ($module)
  3. private can become package-private via synthetic accessors
  4. Enforced at compile time, not by the JVM
  5. Not a security boundary against Java/reflection

basics

~20 s

Kotlin enforces visibility at compile time, then compiles to JVM access flags: public stays public, protected stays protected, but private members are sometimes public in bytecode, and internal becomes public with a mangled name. So Java can sometimes reach what Kotlin hides.

solid answer

~50 s

The JVM has only `public`, `protected`, package-private, and `private` access flags — it has **no `internal`** concept. Kotlin enforces its richer visibility model at **compile time**, then lowers to JVM flags as best it can. `public` → `public`; `protected` → `protected`. **`internal`** → `public` bytecode but **name-mangled** (e.g., `foo$module`) so Java can technically call it but is discouraged. **`private`** members usually map to JVM `private`, but a `private` member accessed by a same-file inline function, lambda, or nested class may be widened to package-private/`public` with a **synthetic accessor** (Kotlin used to generate `access$` bridges; modern Kotlin can also relax the flag). Top-level `private` declarations live in a `FileKt` class as `private`/package-private. The practical consequences: Java interop can bypass Kotlin's encapsulation, reflection sees the relaxed flags, and library authors should not rely on `internal`/`private` as a *security* boundary against JVM-level callers — only as a Kotlin-language contract.

code

kotlin · 10 lines
kotlin
// Kotlin
class Account(internal val number: String) {
    private val pin: String = "0000"
    inline fun describe(block: (String) -> Unit) = block("acct")
}

// What the JVM roughly sees:
//   public getNumber$mymodule()   <- internal, mangled
//   private final pin             <- private (may relax if captured by inline/lambda)
// Java code can call getNumber$mymodule(); Kotlin code in another module cannot call number.

go deeper

for a junior

Knows visibility is enforced by the Kotlin compiler.

for a middle

Knows the JVM lacks internal and that internal compiles to public bytecode.

for a senior

Explains name mangling for internal and that private can be relaxed for inline/lambda capture.

for a principal

Reasons about Java-interop leakage, reflection/serialization implications, binary compatibility of mangled names, and why visibility is a language contract, not a security control.

## The mismatch: Kotlin's model vs the JVM's model Kotlin has four visibilities: `public`, `internal`, `protected`, `private`. The **JVM** has four *different* ones: `public`, `protected`, **package-private** (no keyword), `private`. They don't line up — most notably the JVM has **no `internal`** and Kotlin has **no package-private**. So Kotlin **enforces visibility in the compiler** and then emits the closest JVM **access flags**. ## The mapping | Kotlin | JVM bytecode | Notes | |---|---|---| | `public` | `public` | direct | | `protected` | `protected` | direct (but Java's protected adds package access, so Java callers see slightly more) | | `internal` | `public` + **mangled name** | e.g. `transfer$app`; Kotlin enforces at compile time only | | `private` member | usually `private` | may be **relaxed** when captured by inline functions, lambdas, or nested classes in the same file | | top-level `private` | lives in `XxxKt` class, `private`/package-private | file-scoped | ## Why `private` sometimes isn't `private` in bytecode Kotlin features like **inline functions**, **lambdas**, **local/nested classes**, and **companion objects** may need to read a `private` member from generated bytecode in a *different* synthetic class. Historically the compiler emitted **synthetic accessor methods** (named `access$...`) or widened the field's flag to package-private so the generated class could reach it. So a Kotlin `private` field can appear as package-private/`public` to the JVM, and reflection (`java.lang.reflect`) will report the relaxed flag. ```kotlin class Wallet(private var cents: Long) { // The inline lambda below may force 'cents' to a synthetic accessor fun spend(amount: Long) = run { cents -= amount } } ``` ## Why `internal` is mangled, not hidden Because the JVM can't express 'visible within this module', the compiler keeps `internal` **`public`** so other files in the module can link to it, but **mangles the name** (appends `$moduleName`) to make accidental Java calls unlikely and to avoid signature clashes. Hence: **Kotlin enforces `internal` only at compile time**; Java/JVM tooling can still reach it. ## Why this matters 1. **Java interop leaks encapsulation.** Java code or reflection can reach `internal` (mangled) and sometimes `private` (relaxed) members. Never treat Kotlin visibility as a **security** boundary at the JVM level. 2. **Reflection / serialization frameworks** observe the bytecode flags, not the Kotlin ones — relevant for libraries that scan fields. 3. **API stability**: mangled `internal` names embed the module name, so renaming a module changes the symbol — a binary-compatibility consideration for published JARs. 4. **`@JvmName` / `@JvmStatic`** and friends interact with how names surface to Java, which compounds with mangling. ## Recall - JVM has package-private, **no** internal; Kotlin has internal, **no** package-private. - internal → public + name mangling, compile-time enforced only. - private can be relaxed (synthetic `access$` / flag widening) when inline/lambda/nested code needs it. - Visibility is a language contract, **not** a JVM-level security barrier.

  • Can Java code read a Kotlin internal property?
    Yes, by using the mangled accessor name (e.g., getX$module). Kotlin only enforces internal at compile time; the bytecode is public.
  • Why might a Kotlin private field show as non-private to reflection?
    Inline functions, lambdas, or nested classes in the same file may need access from generated classes, so the compiler relaxes the flag or adds a synthetic access$ method.
  • Is Kotlin visibility a reliable security control?
    No. It's a compile-time language contract; Java interop and reflection can bypass it, so don't rely on private/internal to hide secrets at runtime.

Kotlin visibility is a company's internal org chart (compile-time rules); the JVM only has building keycards (access flags) that don't capture the chart, so some doors are technically unlocked to Java.

saying these in an interview costs you the question

  • Claiming the JVM has a native internal access flag
  • Asserting Kotlin private is always JVM-private with no exceptions
  • Treating internal/private as a runtime security boundary
  • Not knowing internal names are mangled
  • Thinking reflection sees Kotlin-level visibility rather than bytecode flags

context