Basics of Java but Not for Beginners
What happens behind the scenes while running code?
We learn the basics of Java when we start with programming, but if you are like me from a non-CS background, your learnings can often be very unorganized, skipping over a few fundamental things. Or if you are from a CS background, it’s easy to lose sight of what’s happening behind the background since you’ve read it so long ago.
While the point of using any language and not writing machine code is not to go into details about what’s happening behind the scenes, it’s surprising how oblivious we can be while coding to an enormous amount of engineering going on in the background, really fun understanding.
The purpose of this is to be curious, and keep asking why, what, how, when, etc till we can go in-depth into the workings of Java. Think of these as basics for intermediate-level coders. I am writing this because I wish I had this while starting up.

Starting with the most basic question. How does Java work, what happens when you write a hello world program and hit that run button in IntelliJ?
Java compiler javac in JDK will convert our java code to bytecode and then JVM present in JRE will convert bytecode to machine code which our CPU will run.
Now let’s break down this statement. We’ll start from the basics, first principles.
ByteCode & MachineCode
Our CPU understands machine code, that is bits, weird hex things like 0A 00 04 AA.
We are humans and we speak English, we can’t write bytes, so the closest thing we can do is to write in “keywords” which have a specific meaning to them, ex static means something, public means something, etc. These keywords are then mapped to some instructions which CPU can understand.
But Windows, Mac, and Linux are different OS that might be running on different devices with different processors. They work differently and thus understand instructions specific to them. Ex - windows running on 32-bit Intel or 64-bit Intel or ARM will handle memory mappings differently and have commands specific to them.
So what we need is a map of keywords using which we’ll write code and then a Hashmap sort of structure which maps these keywords to what computer can understand which is Hexadecimal (4 bits) and we’ll need multiple such maps depending on what’s running this code, a unique combination of OS and processor.
So to run our Hello World code on Mac and Windows both, we’ll need to convert our code to machine code for Mac first, then same for Windows and similarly for any new machine. We write once and compile everywhere.
But the problem is, if we are selling our Hello World Java app to someone, we want it to run faster, faster than everyone else. What’s the best way to reduce time for anything? Pre-processing. This is exactly how Java solves this problem.
We convert our Java code to an intermediate state so that conversion from here to machine code specific to the CPU is quicker. This intermediary state is byte code and looks similar to the language used to code in 8085 which you would’ve studied in college in the infamous subject - Microprocessors.
No bytecode is not a big file with bits in it. I used to think so. For curious folks, here’s a good explanation. Hello World in Byte Code
So we convert code to bytecode which is then converted to machine code and consumed by CPU.
Again, what do we gain out of this?
Bytecode is closer to machine code and faster to run, think - it’s faster to cook from a half-cooked state
If we had no bytecode, we’d have to hand out either our original Java code with a tool to convert it into machine-specific machine code which is great for open source but a security threat for every other company.
We could directly hand out machine code specific to the machine when someone downloads our app, but we’ll end up with too many versions to maintain.This bytecode will not be understandable by others and harder to convert back to Java code so our top-secret work is safe.
P.S. - You can convert bytecode to Java code using a decompiler but there are safeguards in place to avoid this, read more by googling - “Java bytecode obfuscators” or read more here - OG StackoverflowBytecode can then be converted to machine-specific machine code. We are thus getting “platform independence” as compiling on Windows, Mac or Linux will all result in the same bytecode. This would not be the case if we converted straight to machine code. We are thus getting abstraction over hardware if you think about it. We are writing once and using it everywhere.
Having bytecode means no need to maintain multiple versions of machine code which would become a nightmare to manage especially when we’re constantly updating and releasing security patchwork or fixing bugs.
So now we understand what bytecode, and machinecode are, and why we need both.
Now let’s talk about the players
JDK v/s JRE v/s JVM
JVM is what converts bytecode into machine code and does all the heavy lifting.
JRE is JVM + Core libraries that are everything you need to run Java bytecode. Basically, everything you need to run a java .class file
JDK includes a Java compiler to convert code to bytecode, JRE, additional tools like debugger, etc. Basically, everything you need to create a .class file and run it.
We’ll go into greater (unnecessary) depth in the next article.
So How is Code converted to ByteCode?
This is done by Java compiler which is called Javac.
What does the compiler do here exactly?
Reads the source file as a stream, and makes basic checks like if the file has a .java extension.
Like we read pages by breaking them into lines and lines by breaking them into words, the compiler does a similar thing. It will break the stream into small parts, parts it can make sense of called tokens. This is called lexical analysis or tokenization.
Now like any language, Java code contains
Keywords - static, public, for etc
Identifiers - words which are not keywords - variable names, method names or typos
Other things - literals (1,2,3), operators(+,-,/), punctuations({,[,)
Now the compiler will try and check if the above tokens make sense “grammatically“ in the order they are currently. Example - it gets tokens “if“, “(“, “1“, “)“, “{“, “else“, “{“, “}“, it will check for “grammar”, and if they make sense, here we see we have a missing closing curly brace thus this is wrong, this is how compiler checks for things like missing semi-colons, braces, etc. This is called syntax analysis since we are analysing syntax aka parsing.
Now these tokens further have some priority. Based on the priority, tokens are converted into a syntax tree. For example - max has a priority, and since it takes 2 arguments, we need to first get those arguments in order to process max.
So when Java sees Max, it creates a syntax tree similar to this
int max = Math.max(5, 10);
=
/ \
max Method Call
/ \
Math.max Arguments
/ \
5 10You can visualize the syntax tree for your code here
The use of a syntax tree allows the compiler to organize the code in a traversable way so that it can perform grammatical checks easily.
Now the CPU needs to be fed things one by one, example if we are asking it to do Math.max(5, 10), this is Math.max(t1, t2) so we’ll need to set t1 = 5, t2 = 10, this is what compiler will do, create an intermediate form of code with more clarifications for it to understand, and becomes closer to what a CPU can understand.
But before this, the compiler will do some high-level optimizations, like
Constant Folding - replace your public static variables and constants with the value of constants
Dead Code Elimination - Remove code which is never reached like if-else statements which are always true or false, executing only a part of it, return statements which are never reached, etc.
Inline Expansion - Replaces small, frequently called methods with their actual code bodies to avoid the overhead of method calls.
Why is this done? Every function call has an overhead, example when a function is called, a new stack is set up, arguments are passed to the function, we need to go to the memory location of the new function, etc. Also, inline functions have a higher probability of being near other program bits.
Another thing inline helps with is caching performance. So when compiling takes place, code is placed in RAM, now when the CPU needs to access some data or instructions, it checks if that is present in the cache, otherwise it will bring it into the cache. Now things are brought into the cache in the form of cache lines that are 32-64 bytes of contiguous memory being pulled into the cache. With code inlining, since we inlined everything, everything is located nearby in the RAM (instead of having to load a function again which would be located at a different memory, not in cache), everything is loaded into cache in that cache line thus providing faster access.
Optimizes loop structures to reduce the number of iterations or improve performance (e.g., loop unrolling). Check out Oracle’s Blog for loop unrolling.
Why is this done? Like calling a function, loops also have an overhead, for example on every loop call. Loop overhead includes things like
fetching the loop variable and incrementing it
checking conditions for the loop variable
moving to the start of the loop after every iteration
All these things take time, this can be saved if we reduce the number of times these happen. This can be done by reducing iterations by having more steps done in a single chunk in a loop. Now data loaded into the cache during the first iteration can be reused during the second iteration, helping save time.
Constant Pool Construction
The idea is simple, for every class, we have a lot of keywords, ex - print, int, variable names, etc. All of these take up space, an effective way to store these can be to create a map of these and replace these strings with reference to those map entries and that is what a constant pool is, for every class, a map of keywords. Here’s how
class Hello {
public static void main( String[] args ) {
for( int i = 0; i < 10; i++ )
System.out.println( "Hello from Hello.main!" );
}
}To see the constant pool, use the
javapclass file disassembler included in the JDK. Runningjavapwith the verbose-voption prints a wealth of detail about the class, including the constant pool and the bytecode for all the methods. Runningjavap -v Hello.class, I get a listing of 83 lines. Here is the constant pool portion of that output.
#1 = Methodref #6.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #19 // Hello from Hello.main!
#4 = Methodref #20.#21 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #22 // Hello
#6 = Class #23 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 StackMapTable
#14 = Utf8 SourceFile
#15 = Utf8 Hello.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = Class #24 // java/lang/System
#18 = NameAndType #25:#26 // out:Ljava/io/PrintStream;
#19 = Utf8 Hello from Hello.main!
#20 = Class #27 // java/io/PrintStream
#21 = NameAndType #28:#29 // println:(Ljava/lang/String;)V
#22 = Utf8 Hello
#23 = Utf8 java/lang/Object
#24 = Utf8 java/lang/System
#25 = Utf8 out
#26 = Utf8 Ljava/io/PrintStream;
#27 = Utf8 java/io/PrintStream
#28 = Utf8 println
#29 = Utf8 (Ljava/lang/String;)VWhy is it created?
It’ll take less space, and faster resolution as everything is available in one place in memory, better to hand out a 2 GB jar than a 5 GB jar.
Now this IR (intermediate representation) and constant pool is used to convert our code into bytecode.
How is ByteCode converted to MachineCode?
This is done by JVM.
LOADING
JVM starts with loading the compiled bytecode into memory, pretty basic.
LINKING
Once the bytecode is loaded, the JVM links classes together. This resolves references in the code to actual memory addresses, connects method calls to their definitions, and ensures all parts of the program know where to find each other. Think of it as assembling the pieces of a puzzle so they fit together correctly.
VERIFICATION
Once loaded, JVM verifies that the bytecode is valid, nothing illegal is going on like code accessing memory locations it’s not supposed to. Every JVM has a class file verifier, ensuring the class file is valid.
PREPARATION
During preparation, the JVM allocates memory for class variables and sets default values. Static fields are given initial space, and all necessary data structures are set up. This step doesn’t run any code yet—it simply makes sure that everything is in place for when the program starts executing.
RESOLUTION
Say your class refers to another class, loading of those classes happens here. The JVM loads these referenced classes, linking their definitions into the runtime environment. This ensures that when a method or field from another class is needed, the JVM knows exactly where to find it.
INITIALIZATION
Now it's time to run the code that initializes classes. For each class, the JVM starts with its superclass, moving down the chain:
Static fields that were set to default values during preparation are now assigned their actual declared values.
Static blocks and initializers run, performing any setup required by the class. Initialization can occur at various times, such as when creating a new instance, invoking a static method, or accessing a static field for the first time.
Initialization of a class occurs anytime
An instance of the class is created
Static field of that class is assigned
Static method of that class is invoked
Non - constant static field of that class is used. Remember all constants (static final) are embedded in the byte-code directly during constant folding.
INSTANTIZATION
When your code creates a new object (using the new keyword), the JVM takes several steps:
It allocates memory on the heap for the new object.
It calls the class’s constructor to initialize the object.
It returns a reference to the new object so your program can use it. If those constructors or methods create additional objects (like concatenating strings or boxing primitives), the JVM repeats this process for each new instance created on the fly.
JIT Compilation (Just in Time)
The JVM uses a Just-In-Time (JIT) compiler to translate bytecode into machine-specific code on the fly. Instead of interpreting bytecode line-by-line every time, the JIT compiler compiles frequently executed sections (hot spots) into optimized machine code. This machine code runs directly on the CPU, offering significant performance boosts. The JIT compilation blends interpretation with compilation, adapting to the running program’s behaviour. Here’s a nice video from Computerphile
UNLOADING
Finally, once a class is no longer needed, no instances exist and it’s eligible for garbage collection, the JVM will unload the class, freeing up resources.
I was unfortunate enough to struggle with memory leaks at work so more depth on garbage collection soon.
Rabbit Hole
Constant Pool - https://blogs.oracle.com/javamagazine/post/java-class-file-constant-pool
JIT - YT Video
Hello World ByteCode - https://medium.com/@davethomas_9528/writing-hello-world-in-java-byte-code-34f75428e0ad

