Layers of Logic

1.2

JVM, JRE, JDK, and Your First Program

Build and run one Java file from the terminal, then follow it through the runtime.

Core18 min read5 exercises
01

Previously on

Section 1.1 established the route from source code to a platform:

source -> bytecode -> JVM -> processor and operating system

This lesson makes that route concrete. You will create one file, compile it, inspect the result, and run it without an IDE.

02

The problem

Java installation instructions use three similar names: JVM, JRE, and JDK. Mixing them up creates practical confusion.

Someone installs a runtime and expects to find a compiler. Another person passes a .class file to javac. An IDE hides both mistakes until a terminal or server exposes them.

We need one accurate model, then one complete trip through the tools.

03

The idea

The three names

JDKJava Development Kit
  • javac compiler
  • java launcher
  • inspection and debugging tools
  • runtime modules

Install this when you develop Java programs.

JREJava Runtime Environment
  • JVM
  • standard runtime libraries
  • supporting runtime files

The traditional package for running Java applications.

JVMJava Virtual Machine
  • class loading
  • bytecode execution
  • managed memory

The engine defined by the JVM specification and implemented by a runtime.

The conceptual nesting is still useful. Modern JDKs may not ship a separate folder or download named JRE.

Use these working definitions:

  • The JVM executes class files under the JVM rules.
  • A runtime environment adds the libraries and files applications need.
  • The JDK adds tools used to build, inspect, and debug programs.

For this course, install a JDK. It contains what you need for both development and execution.

Confirm the installation

Open a terminal and run:

java -version
javac -version

The first command checks the launcher. The second checks the compiler.

If java works but javac does not, you may have only a runtime on your command path. If neither works, the JDK may be missing or its bin directory may not be on PATH.

PATH is the list of directories your shell searches for commands. It is an operating-system feature, not a Java keyword.

Write a complete program

Create a plain text file named Demo.java:

public class Demo {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

Every part has a job:

public class Demo                 declares a class named Demo
public static void main(...)      declares the program entry method
String[] args                     receives command-line arguments
System.out.println(...)           prints one line

You do not need the full meaning of public, static, or String[] yet. Their later lessons will rebuild this line from known parts.

One rule matters now: a public top-level class named Demo belongs in Demo.java. Capitalisation must match.

Compile it

Change the terminal to the directory containing the file. Run:

javac Demo.java

If compilation succeeds, the command usually prints nothing. It creates Demo.class in the same directory.

You now have two different artifacts:

Demo.javaDemo.class
ContainsJava source textClass-file data and bytecode
Made forHumans and javacA compatible JVM
Typical actionEdit and compileLoad and execute

Run it

Run the class through the Java launcher:

java Demo

Expected output:

Hello, Java!

javac accepts a file name, including .java. The java launcher accepts a binary class name, so you write Demo without an extension.

04

Under the hood

Going deeper

Compile-time errors and run-time errors are different

Remove the semicolon after println, then run javac Demo.java. The compiler reports a source error and does not produce a new valid class file.

Restore the semicolon, compile, then run java Missing. The launcher cannot find a class with that name.

These failures occur at different stages:

Find the failing stage

  1. Source parsing and type checkingjavac reports invalid Java source.
  2. Class lookupThe launcher reports that the requested class cannot be found.
  3. Entry-point lookupThe class exists, but no suitable main method exists.
  4. Program executionThe program starts and may then throw an exception.

Reading the stage is faster than treating every red message as the same problem.

The class path answers “where?”

By default, java Demo searches the current directory for Demo.class. That search location is part of the class path.

You can state the current directory explicitly:

java -cp . Demo

-cp means class path. The dot means the current directory.

Packages later add directory structure and qualified names. The same lookup rule remains: the runtime needs both the class name and a place to search.

What the runtime does with the class

Before your first println executes, the runtime performs several jobs.

  1. Loading: locate the bytes for Demo and create the JVM’s in-memory representation.
  2. Verification: check structural and bytecode safety rules.
  3. Linking: prepare class data and resolve required symbolic references.
  4. Initialization: run required static initialization for the class.
  5. Invocation: call the valid main method.

That is why java Demo does more than open a file and read instructions from the first byte.

Interpreting and compiling at run time

javac is an ahead-of-time compiler from Java source to JVM bytecode. That makes Java a compiled language.

A common JVM can also interpret bytecode and compile selected methods into native code while the program runs. The second compiler is called a just-in-time compiler, or JIT compiler.

Runtime profiling lets the JVM spend optimisation effort where the program actually spends time. Code that rarely runs may not need expensive compilation.

These are implementation strategies. The JVM specification defines observable behaviour, not one mandatory balance between interpretation and JIT compilation.

You can inspect the class file without decoding raw bytes:

javap -c Demo

javap is a JDK tool. The -c option prints readable bytecode instructions for each method.

SE, EE, and ME describe platforms and ecosystems

Java SE is the standard Java platform and core API used throughout this course.

Jakarta EE, historically called Java EE, adds specifications for enterprise server applications. It builds on Java SE rather than replacing it.

Java ME targets constrained and embedded environments. Its role is much smaller in modern general-purpose Java development.

These labels are not three different Java languages. They describe platform profiles, APIs, and deployment environments around the language.

05

What it costs

An IDE can compile and launch Java for you. That is useful after you understand the two commands it is running.

Learning the terminal route exposes file names, class names, stale class files, and class-path mistakes. Those details return in build tools, test runners, servers, and production diagnostics.

The small cost now prevents the development environment from becoming a black box.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. You want to write, compile, and run Java programs. Which package do you install?

    Show the answer

    Install a JDK. It provides the Java runtime, the javac compiler, and development tools. A JVM is the execution engine. JRE is the traditional name for a JVM plus runtime libraries and supporting files.

  2. Why does compilation use "javac Demo.java", while execution uses "java Demo"?

    Show the answer

    javac compiles a source file, so it receives the file path. The launcher receives a binary class name. It searches the class path for Demo.class, loads that class, and calls its entry method.

  3. What happens before the first statement in main runs?

    Show the answer

    The launcher starts a JVM. The runtime locates and loads the class, checks its bytecode, links it, and initializes required class state. It then invokes a valid main method.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

5 exercises90 pointsabout 100 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Prove You Have a JDK

Warm up·15 min·10 points

ex-1-2-a

Get Java working on your own machine, and then prove it.

Do not skip the proving part. “I installed it” is not the same as “it works”, and the difference between those two commands is the whole JRE versus JDK idea made real.

If something breaks, search for the error, read what people say, try things. Getting an install unstuck is a normal part of this job, and doing it once yourself is worth more than watching somebody else do it.

What your program must do

  • Install JDK 21, which is the version used by this course's exercise checker
  • Run both version commands and get a real version number from each
  • Write down which JDK version you have
  • Explain in one line why passing only the second command is not good enough

Sample run

You type
java -version
javac -version
It prints
openjdk version "21.0.12"
javac 21.0.12
Hint 1
Install it however you like. Oracle's site works. So does Homebrew on Mac (brew install openjdk), or your package manager on Linux, or winget on Windows. Search for it, try it, fix what breaks. That is a real skill.
Hint 2
If java -version works but javac -version says command not found, you have a JRE on your path but not a JDK. Or the JDK is installed but its bin folder is not on your PATH.
Hint 3almost the answer
java is the launcher and javac is a JDK development tool. If only java works, your command path does not expose a complete JDK toolchain.
What this is really testing

Whether you can tell a working install from a broken one. Half of all "Java is not working" problems are really "the compiler is not on the path", and the two commands here tell those apart in five seconds.

B

Your First Program, the Slow Way

Warm up·15 min·10 points

ex-1-2-b

Write, compile and run a Java program using nothing but a text editor and a terminal.

Do not use the Run button in VS Code or IntelliJ for this one. Use it for the rest of your life if you want, but do this one by hand.

The reason is simple. That green button runs javac and then java for you and hides both. If you have never seen the two steps separately, then compiling and running are one mysterious action in your head, and every error message from either one will look the same to you.

Do it once by hand and they become two clearly different things forever.

What your program must do

  • Save the file as Demo.java, with a capital D
  • Compile it from the terminal, not with a Run button
  • Confirm that Demo.class appeared next to Demo.java
  • Run it from the terminal and see the output
Demo.java
public class Demo {
    public static void main(String[] args) {
        System.out.println("Hello from bytecode");
    }
}

Sample run

You type
javac Demo.java
java Demo
It prints
Hello from bytecode
Hint 1
Open a terminal and move into the folder that holds the file first. On Mac and Linux use cd. On Windows, Command Prompt uses cd too.
Hint 2
After javac Demo.java nothing is printed. That is success. A compiler that has nothing to complain about says nothing. Check with ls or dir that Demo.class exists.
Hint 3almost the answer
The run command is java Demo. Not java Demo.java, and not java Demo.class. The java command wants a class name, and it finds the file itself.
What this is really testing

Whether you can run Java without an IDE hiding the steps. Every green Run button in every editor is doing exactly these two commands, and you should see them at least once with your own eyes.

C

Look at the Bytecode

Real work·20 min·20 points

checkedex-1-2-c

You have been told your .java file becomes bytecode. Now go and look at it.

Compile Demo.java from the previous exercise, then run javap on the result. First plain, then with -c to see the real instructions.

You are not expected to understand every line. You are expected to see, with your own eyes, that there is a readable middle language between your source code and the processor. That it is not zeros and ones. That it has names like ldc and invokevirtual.

Keep the output somewhere. You will come back to it twice: once in Phase V, once in Phase VII.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Bytecode {
    static String instructionThatLoadsAConstant()
    static String instructionThatReadsAStaticField()
    static String instructionThatCallsAMethodOnAnObject()
    static String instructionThatCallsAStaticMethod()
    static String instructionThatCallsAConstructor()
    static String instructionThatEndsAMethod()
    static boolean classFileHasAConstructorYouDidNotWrite()
    static String classThatHiddenConstructorCalls()
    static boolean bytecodeIsMachineCodeForYourProcessor()
    static boolean classFileOnDiskIsBinary()
}

Instruction names exactly as javap prints them, with no numbers and no hash marks. Capitals and surrounding spaces are ignored. For the class name, either Object or the full java/lang/Object is accepted. One answer needs you to add a static method to Demo and compile it again, because the sample output has no static call in it.

What your program must do

  • Run javap on your compiled class, first plain and then with -c
  • Find the instruction that pushes your text onto the stack
  • Add a static method, recompile, and find the third kind of call
  • Write down what the first block is, given that you never wrote a constructor
Bytecode.java
public class Bytecode {

    // Compile Demo.java from the previous exercise, then run:
    //     javap Demo
    //     javap -c Demo
    // Read the output and fill these in. Instruction names only, no numbers.

    // TODO: which instruction holds your text?
    static String instructionThatLoadsAConstant() { return "?"; }

    // TODO: which one reads System.out?
    static String instructionThatReadsAStaticField() { return "?"; }

    // TODO: which one calls println on that object?
    static String instructionThatCallsAMethodOnAnObject() { return "?"; }

    // TODO: add a static method to Demo, call it, recompile, and look again
    static String instructionThatCallsAStaticMethod() { return "?"; }

    // TODO: look at the first block, the one named after the class itself
    static String instructionThatCallsAConstructor() { return "?"; }

    // TODO: which instruction ends every method in the output?
    static String instructionThatEndsAMethod() { return "?"; }

    // ---- the thing at the top you did not write ----

    // TODO
    static boolean classFileHasAConstructorYouDidNotWrite() { return false; }

    // TODO: which class does it call into? Write down the answer and move on.
    static String classThatHiddenConstructorCalls() { return "?"; }

    // ---- what this tells you ----

    // TODO: is bytecode machine code for the processor in your laptop?
    static boolean bytecodeIsMachineCodeForYourProcessor() { return true; }

    // TODO: open Demo.class in a text editor. Is the file itself readable?
    static boolean classFileOnDiskIsBinary() { return false; }

    public static void main(String[] args) {
        System.out.println("load a constant     : " + instructionThatLoadsAConstant());
        System.out.println("read a static field : " + instructionThatReadsAStaticField());
        System.out.println("call on an object   : " + instructionThatCallsAMethodOnAnObject());
    }
}

Sample run

You type
javap -c Demo
It prints
public class Demo {
  public Demo();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #7   // Field java/lang/System.out
       3: ldc           #13  // String Hello from bytecode
       5: invokevirtual #15  // Method java/io/PrintStream.println
       8: return
}
Hint 1
javap comes with your JDK, so you already have it. Run it in the same folder as Demo.class, and give it the class name with no extension: javap -c Demo.
Hint 2
ldc is short for load constant. getstatic reads a static field. The three call instructions all begin with invoke, and which one appears depends on whether there is an object involved and whether anything has to be looked up.
Hint 3almost the answer
Look at the very first block, the one named after the class. You never wrote a constructor, and there is one, and it calls into java/lang/Object. Do not chase this now. Write it down. In Section 8.3 you will find out why every class you write already extends Object, and this output is the proof.
What this is really testing

Whether bytecode is a real thing to you or just a word. You have been told a middle language exists. This is you opening it and reading it.

D

Break It On Purpose

Real work·20 min·20 points

checkedex-1-2-d

Deliberately breaking things is how you learn what error messages mean. Right now you have a working program, which makes this the perfect time.

Break it four ways. After each one, fix it before moving on.

  1. Delete a semicolon and compile.
  2. Rename the class inside the file to Hello, but leave the file named Demo.java. Compile.
  3. Change main to mian, compile it (it will compile fine), then run it.
  4. Compile normally, then delete Demo.class, then run java Demo.

Number 3 is the interesting one. Ask yourself why the compiler was happy with a method called mian and only the JVM complained.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Break {
    static String whoReported(String message)
    static String whoReportedMissingSemicolon()
    static String whoReportedFilenameMismatch()
    static String whoReportedMisspeltMain()
    static String whoReportedMissingClassFile()
    static boolean compilerChecksThatMainExists()
    static boolean javacMessagesNameASourceLine()
    static boolean jvmMessagesNameASourceLine()
}

Every answer is "javac" or "jvm", and capitals and surrounding spaces are ignored. whoReported must be a rule rather than a list of the four messages, because the tests feed it errors you have not caused. One of those is a stack trace, which does print a line number and is still the JVM talking, so look closely at what surrounds the line number in a real compiler error.

What your program must do

  • Cause all four failures deliberately and copy the exact message
  • Say whether javac or the JVM produced each one
  • Write whoReported as a rule that works on messages you have not seen
  • Explain why javac was happy with a method called mian
Break.java
public class Break {

    // TODO: the rule, worked out from the four messages you collected.
    // Return "javac" or "jvm".
    static String whoReported(String message) {
        return "?";
    }

    // ---- the four failures you caused ----
    // Cause each one, copy the exact message, then say who produced it.

    // TODO: 1. delete a semicolon and compile
    static String whoReportedMissingSemicolon()  { return "?"; }

    // TODO: 2. rename the class inside Demo.java to Hello, and compile
    static String whoReportedFilenameMismatch()  { return "?"; }

    // TODO: 3. change main to mian, compile (it will work), then run
    static String whoReportedMisspeltMain()      { return "?"; }

    // TODO: 4. compile, delete Demo.class, then run java Demo
    static String whoReportedMissingClassFile()  { return "?"; }

    // ---- what that tells you ----

    // TODO: number 3 compiled. Does javac check that a main method exists?
    static boolean compilerChecksThatMainExists() { return true; }

    // TODO: what does a javac message always contain that a JVM message never does?
    static boolean javacMessagesNameASourceLine() { return false; }
    static boolean jvmMessagesNameASourceLine()   { return true; }

    public static void main(String[] args) {
        // Paste your four real messages in here and check your rule against them.
        String[] messages = {
            "TODO",
        };
        for (String m : messages) {
            System.out.println(whoReported(m) + "  <-  " + m);
        }
    }
}
Hint 1
javac errors talk about your source code. JVM errors talk about classes and appear only when you try to run. Line up your four messages next to each other and the difference is in the first few characters.
Hint 2
A real compiler error reads Demo.java:3: error: ';' expected. Three parts: the file, the line, then the word error. A JVM message has none of that shape, because the JVM never opened your source file.
Hint 3almost the answer
Watch out for the stack trace case. at Demo.main(Demo.java:4) contains a file and a line number and is still the JVM talking. The part only the compiler writes is the : error: straight after the line number.
What this is really testing

Whether you can read an error message and say which tool produced it. A compiler error and a JVM error mean completely different things, and telling them apart instantly will save you hours for the rest of your career.

E

Compare Tiered Execution With Interpreter-Only Mode

Hard·30 min·30 points

ex-1-2-e

This section explained that a JVM may interpret code and compile active methods while the program runs. This exercise observes one implementation of that strategy.

The starter repeats one method and prints the duration of each round. Run it several times normally, then repeat with interpreter-only mode.

Do not require a dramatic cliff. The JVM may compile during an early call, and the operating system adds noise. Use compilation logs when supported and explain what the experiment can and cannot prove.

What your program must do

  • Run the program three times and record all 20 rounds from each run
  • Run it three times with -Xint and record the same data
  • If your JVM supports it, use -XX:+PrintCompilation to observe compilation events
  • Explain the difference and name two reasons the timings are noisy
Warmup.java
public class Warmup {
    // Some work that is worth compiling. Deliberately simple.
    static long work(int n) {
        long total = 0;
        for (int i = 0; i < n; i++) {
            total += i % 7;
        }
        return total;
    }

    public static void main(String[] args) {
        for (int round = 1; round <= 20; round++) {
            long start = System.nanoTime();
            long result = work(2_000_000);
            long took = (System.nanoTime() - start) / 1_000_000;
            System.out.println("round " + round + ": " + took + " ms  (result " + result + ")");
        }
    }
}

Sample run

It prints
round 1: 14 ms
round 2: 9 ms
round 3: 8 ms
round 4: 2 ms
round 5: 1 ms
...
round 20: 1 ms
Hint 1
Run it plainly first: java Warmup. Early rounds may be slower, but a sharp drop is not guaranteed. Compilation can begin during the first measured call.
Hint 2
Now run java -Xint Warmup. The -Xint flag tells the JVM to use only the interpreter and never the JIT compiler. Compare the two sets of numbers.
Hint 3almost the answer
On HotSpot, java -XX:+PrintCompilation Warmup prints compilation events. Other JVMs may use different flags. Scheduling, CPU frequency, background work, timer resolution, and JIT activity all affect timings. Compare repeated runs and describe a trend rather than claiming an exact threshold.
What this is really testing

Whether you can measure two JVM execution modes without treating one noisy timing run as a language guarantee.

08

After the credits

You have completed the first full Java toolchain cycle:

Demo.java -> javac -> Demo.class -> java -> class loading -> main

The next phase begins with the values that move through those bytecode instructions.

Threads you opened in this section

JVM will return in 4.2 - How Arrays Work