When I first started learning Java, I was bombarded with a number of words and acronyms that all sounded the same – JDK, JRE, JVM, J2EE, J2SE and so on….. what do they all mean?

This can often be intimidating for beginners, who often just skip ahead and jump straight into an IDE that hides all the details behind a pretty UI.

But I think it’s important to understand the ecosystem, so that you can understand what’s going on under the hood, and build better software.

Let’s start with the most basic building block of your Java application:

Bytecode

The first thing you should know is that the code you write in Java compiles into something called bytecode.

Bytecode is similar to binary or machine code, but unlike machine code, it can’t be executed directly by the CPU. Instead, it needs to be interpreted by a special program called a Java Virtual Machine.

For example, consider the following program stored in the file HelloWorld.java:

package com.sohamkamani;

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

We can use the javac command to compile this program:

$ javac HelloWorld.java

This will generate a new file called HelloWorld.class in the same directory, which contains the bytecode for our program.

bytecode compilation

💡 When you install Java on your machine, it’ll come bundled with a number of commands that you can use, like javac, javap, and java. We’ll be using these commands throughout this article.

You can even see this bytecode by running the javap command:

$ javap -c HelloWorld.class
Compiled from "HelloWorld.java"
public class com.sohamkamani.HelloWorld {
  public com.sohamkamani.HelloWorld();
    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:Ljava/io/PrintStream;
       3: ldc           #13                 // String Hello World!
       5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

Bytecode is not supposed to be human-readable, so don’t worry if you don’t understand what’s going on here. The important thing to note is that this is low level code that will be executed by a Java Virtual Machine.

Java Virtual Machine (JVM)

As we saw in the previous section, bytecode is not executable by the CPU. Instead, it needs to be interpreted by a special program – which is the Java Virtual Machine.

The JVM is a program that runs on your machine, and is responsible for executing your Java code. It’s also responsible for a number of other things, like memory management, garbage collection, and security.

You can think of the JVM as a translator that takes your bytecode and converts it into something that the CPU can understand.

jvm interpreting bytecode on a machine

But why do we need Bytecode and the JVM in the first place? Can’t we just compile our Java code into machine level code directly like other compiled languages?

The main value of the JVM is that it allows us to write code that can run on any machine, regardless of the underlying hardware or operating system. This means that as long as we have the JVM installed, we can be sure that our code will run the same way on any machine.

This is a huge advantage over other compiled languages like C or C++, where you need to compile your code separately for each platform, which can lead to a lot of compatibility issues.

Java Runtime Environment (JRE)

In our previous example, we ran this line of Java code:

System.out.println("Hello World!");

We often take this for granted, but let’s think about what’s actually happening here.

Here, System.out.println is the function that prints our message to the console. But we never defined this function anywhere in our code. So where does it come from?

The answer is that it’s part of the Java Runtime Environment (JRE). The JRE is a collection of libraries and components that are responsible for making sure your Java code executes efficiently.

Note that the JRE also includes the JVM, so when you install the JRE on your machine, you’ll also get the JVM.

jre components

The JVM here, can be thought of as just an interpreter, reading instructions and executing them in the native OS. The program lifecycle itself, like scheduling, memory management, and security, is handled by the JRE.

Java Development Kit (JDK)

While the JRE is enough to run Java code, it doesn’t include the tools you need to develop Java code. For that, you’ll need the Java Development Kit (JDK).

For example, the javac command that we used to compile our code is part of the JDK. The JDK also includes a number of other tools that you can use to develop Java code, like the javadoc command for generating documentation, or the jshell command for running Java code interactively.

The JDK also includes the JRE, so when you install the JDK on your machine, you’ll also get the JRE and, in turn, the JVM.

jdk components

How Java Runs Your Code

Let’s go back to the example we saw earlier and see how the JDK, JRE, and JVM work together to run our code.

First, let’s see the directory structure of our project:

└── src
    └── com
        └── sohamkamani
            └── HelloWorld.java

You’ll often see this directory structure in Java projects. The src directory contains the source code for our project, and the HelloWorld.java file contains the code we saw earlier.

When working with Java, the package name of your class should match the directory structure of your project. So in this case, the package name of our class is com.sohamkamani, which means that the HelloWorld.java file should be located in the src/com/sohamkamani directory.

When we run the javac command, the JDK will compile our code into bytecode, and save it in a file called HelloWorld.class.

When we run the java command, the JVM will load the bytecode from the HelloWorld.class file, and execute it:

$ java -cp ./src/ com.sohamkamani.HelloWorld

The -cp flag is used to specify the classpath, which is the location where the JVM will look for the bytecode. Or in other words, it is the location after which your folder structure should match the package name.

In this case, we’re telling the JVM to look for the bytecode in the src directory, and run the com.sohamkamani.HelloWorld class, which it will expect to find in the src/com/sohamkamani directory.

What is Java SE and Java EE?

As we’ve seen previously, Java applications can’t be developed in isolation, and in almost all cases, you’ll need to use a number of libraries and components that are provided by the JRE.

Java SE (Standard Edition) and Java EE (Enterprise Edition) are two different sets of libraries that you can use in your Java code.

Java SE is always included with the JRE and JDK, and includes the core libraries that you’ll need to develop most Java applications.

Java EE is a set of libraries that are used for developing enterprise applications, like web applications or distributed applications. It includes a number of additional libraries that you can use in your code.

The word “enterprise” is more of a marketing term, which is used to describe a bunch of functionality needed by a large scale application, such as:

  1. Security - Authentication, authorization, and encryption, provided by jakarta.security.auth.message
  2. Concurrency - Multi-threading and parallel processing, provided by jakarta.enterprise.concurrent
  3. Persistence - Database access and object-relational mapping, provided by jakarta.persistence
  4. Messaging - Asynchronous communication between components, provided by jakarta.jms
  5. Web Servers - Web servers and servlet containers, provided by jakarta.servlet

And so on…

This is actually something that I really appreciate about the Java ecosystem – most of the libraries that you’ll need to develop a large scale application are already included in the standard library, so you don’t need to install a bunch of third party libraries to get started.

Java EE is now Jakarta EE

In 2017, Oracle announced that they would be transferring the Java EE platform to the Eclipse Foundation, where it would be developed under the name Jakarta EE.

This is why you’ll see the jakarta prefix in the Java EE libraries, instead of the javax prefix that was used in earlier versions.

So basically: J2EE, Java EE, and Jakarta EE are all different versions of the same library.

JAR Files

JAR files are a way to package multiple class files into a single file, which makes it easier to distribute and run your code. But why do we need it?

The Problem with Multiple Class Files

In the first example, we compiled our code into a file called HelloWorld.class, and we were able to execute the class file directly using the java command.

In reality though, we’ll have multiple class files in our project, and distributing them as separate files can be tricky.

To illustrate the issue, and why we need JAR files, let’s add a new HelloUniverse class to our project:

package com.sohamkamani;

public class HelloUniverse {
  public static void sayHello() {
    System.out.println("Hello Universe!");
  }
}

So, our folder structure looks like this:

.
└── src
    └── com
        └── sohamkamani
            ├── HelloUniverse.java
            └── HelloWorld.java

And we can compile our code using the javac command:

$ javac src/com/sohamkamani/*.java

This will generate two class files, HelloWorld.class and HelloUniverse.class in the same locations:

.
└── src
    └── com
        └── sohamkamani
            ├── HelloUniverse.class
            ├── HelloUniverse.java
            ├── HelloWorld.class
            └── HelloWorld.java

And, we could run our code using the java command:

$ java -cp ./src/ com.sohamkamani.HelloWorld
Hello World!
Hello Universe!

So far so good. But what if we wanted to distribute our code to someone else? We could just send them the HelloWorld.class and HelloUniverse.class files, and they could run it using the java command, right?

Well, not exactly. These files need to be in the same directory structure as our package name, so we’d have to send them the entire src directory, which is not ideal.

This gets even more complicated when we start using third party libraries, which will have their own directory structure and package names.

The Solution: JAR Files

This is where JAR files come in. JAR files are a way to package multiple class files into a single file, which makes it easier to distribute and run your code.

To create a JAR file, we can use the jar command:

$ jar --create --file=app.jar --main-class=com.sohamkamani.HelloWorld -C src .

Let’s look at what each of these options mean:

  1. --create - Create a new JAR file
  2. --file=app.jar - Specify the name of the JAR file that we want to create
  3. --main-class=com.sohamkamani.HelloWorld - Specify the main class that we want to run. This needs to be the fully qualified name of the class that contains the main method. Under the hood, this will create a special file named META-INF/MANIFEST.MF in the JAR file, which will tell the JVM which class to run when we execute the JAR file.
  4. -C src . - Specify the location of the class files that we want to include in the JAR file. In this case, we’re telling the jar command to look for the class files in the src directory, and include them in the JAR file. Note that we use src because that’s the location after which our folder structure matches the package name.

jar file structure

This will create a new file called app.jar, which we can run using the java command:

$ java -jar app.jar       
Hello World!
Hello Universe!

Now, if we want someone else to run our code, we can just send them the single app.jar file, and they can run it using the java command.

The JAR file is actually just a ZIP file, and you can open it and explore its files using any ZIP utility program.

Third Party Tools

Everything we discussed so far is part of the standard Java ecosystem, and is included with the JDK. But there are also a number of third party tools that you should know about to develop Java applications.

Build Tools

Build tools, like Maven, Gradle, and Ant are used to automate the process of compiling and packaging your code.

Each of these tools have their own features and syntax, but they all work on a similar principle - you specify how you want to build your application in a configuration file, and then run the build tool to compile and package your code automatically.

The configuration file contains information like:

  1. The location of your Java files
  2. The location of your third party libraries
  3. The location where you want to save the compiled code, and packaged JAR file
  4. The version of the JDK that you want to use

This makes sure that everyone on your team is using the same version of the JDK, and that the code is compiled in the same way – which is especially important when you’re working on a large project with multiple developers.

IDEs

IDEs, like Eclipse, IntelliJ IDEA, and NetBeans are programs that provide an easy to use UI for developing Java applications.

In fact, most people who use an IDE don’t even need to know anything about the Java ecosystem, because the IDE will take care of everything for them.

For example, when you create a new project in IntelliJ IDEA, it will automatically create a build configuration file for you, and you can compile and run your code directly from the UI itself.

Personally, I prefer to use a text editor like VS Code or Sublime Text for writing code, and then use the command line to compile and run my code - if you’re interested, you can see the project structure and workflow that I like to use.

However, if you’re just getting started with Java, I’d recommend using an IDE, because it will make your life a lot easier.

Everything Else

Java has a rich ecosystem of tools and libraries, and there are a number of other tools that you might need to use, depending on the type of application you’re building.

Since Java is such a mature language, that means that there is almost certainly a library for whatever you’re trying to do.

For example, if you’re building a web application, you might want to use a web framework like Spring, or if you’re building a desktop application, you might want to use a GUI framework like JavaFX or Swing.

For unit testing, you might want to use a library like JUnit, and for logging, you might want to use a library like Log4j.