JavaRush /Java Blog /Random EN /Compiling and executing Java applications under the hood
Павел Голов
Level 34
Москва

Compiling and executing Java applications under the hood

Published in the Random EN group

Content:

  1. Introduction
  2. Compiling to bytecode
  3. Example of program compilation and execution
  4. Executing a program on a virtual machine
  5. Just-in-time (JIT) compilation
  6. Conclusion
Compiling and executing Java applications under the hood - 1

1. Introduction

Hi all! Today I would like to share knowledge about what happens under the hood of the JVM (Java Virtual Machine) after we run a Java written application. Nowadays, there are trendy development environments that help you avoid thinking about the internals of the JVM, compiling and executing Java code, which can cause new developers to miss these important aspects. At the same time, questions regarding this topic are often asked during interviews, which is why I decided to write an article.

2. Compilation to bytecode

Compiling and executing Java applications under the hood - 2
Let's start with the theory. When we write any application, we create a file with an extension .javaand place code in it in the Java programming language. Such a file containing human readable code is called a source code file . Once the source code file is ready, you need to execute it! But at the stage it contains information understandable only to humans. Java is a multi-platform programming language. This means that programs written in Java can be executed on any platform that has a dedicated Java runtime system installed. This system is called Java Virtual Machine (JVM). In order to translate a program from source code into code that the JVM can understand, you need to compile it. The code that the JVM understands is called bytecode and contains a set of instructions that the virtual machine will subsequently execute. To compile source code into bytecode, there is a compiler javacincluded in the JDK (Java Development Kit). As input, the compiler accepts a file with the extension .java, containing the source code of the program, and as output, it produces a file with the extension .class, containing the bytecode necessary for the program to be executed by the virtual machine. Once a program has been compiled into bytecode, it can be executed using a virtual machine.

3. Example of program compilation and execution

Suppose we have a simple program, contained in a file Calculator.java, that takes 2 numeric command line arguments and prints the result of their addition:
class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
In order to compile this program into bytecode, we will use the compiler javacon the command line:
javac Calculator.java
After compilation, we receive a file with bytecode as an output Calculator.class, which we can execute using the java machine installed on our computer using the java command on the command line:
java Calculator 1 2
Note that after the file name, 2 command line arguments were specified - numbers 1 and 2. After executing the program, the number 3 will be displayed on the command line. In the example above, we had a simple class that lives on its own. But what if the class is in some package? Let's simulate the following situation: create directories src/ru/javarushand place our class there. Now it looks like this (we added the package name at the beginning of the file):
package ru.javarush;

class Calculator {
    public static void main(String[] args){
        int a = Integer.valueOf(args[0]);
        int b = Integer.valueOf(args[1]);

        System.out.println(a + b);
    }
}
Let's compile such a class with the following command:
javac -d bin src/ru/javarush/Calculator.java
In this example, we used an additional compiler option -d binthat puts the compiled files into a directory binwith a structure similar to the directory src, but the directory binmust be created in advance. This technique is used to avoid confusing source code files with bytecode files. Before running the compiled program, it is worth explaining the concept classpath. Classpathis the path relative to which the virtual machine will look for packages and compiled classes. That is, in this way we tell the virtual machine which directories in the file system are the root of the Java package hierarchy. Classpathcan be specified when starting the program using the flag -classpath. We launch the program using the command:
java -classpath ./bin ru.javarush.Calculator 1 2
In this example, we required the full name of the class, including the name of the package in which it resides. The final file tree looks like this:
├── src
│     └── ru
│          └── javarush
│                  └── Calculator.java
└── bin
      └── ru
           └── javarush
                   └── Calculator.class

4. Execution of the program by a virtual machine

So, we launched the written program. But what happens when a compiled program is launched by a virtual machine? First, let's figure out what the concepts of compilation and code interpretation mean. Compilation is the translation of a program written in a high-level source language into an equivalent program in a low-level language similar to machine code. Interpretation is an operator-by-statement (command-by-line, line-by-line) analysis, processing and immediate execution of the source program or request (as opposed to compilation, in which the program is translated without executing it). The Java language has both a compiler ( javac) and an interpreter, which is a virtual machine that converts bytecode into machine code line by line and immediately executes it. Thus, when we run a compiled program, the virtual machine begins to interpret it, that is, line-by-line conversion of bytecode into machine code, as well as its execution. Unfortunately, pure bytecode interpretation is quite a long process and makes java slow compared to its competitors. To avoid this, a mechanism was introduced to speed up the interpretation of bytecode by the virtual machine. This mechanism is called Just-in-time compilation (JITC).

5. Just-in-time (JIT) compilation

In simple terms, the mechanism of Just-In-Time compilation is this: if there are parts of the code in the program that are executed many times, then they can be compiled once into machine code to speed up their execution in the future. After compiling such a part of the program into machine code, with each subsequent call to this part of the program, the virtual machine will immediately execute the compiled machine code rather than interpret it, which will naturally speed up the execution of the program. Speeding up the program is achieved by increasing memory consumption (we need to store the compiled machine code somewhere!) and by increasing the time spent on compilation during program execution. JIT compilation is a rather complex mechanism, so let's go over the top. There are 4 levels of JIT compilation of bytecode into machine code. The higher the compilation level, the more complex it is, but at the same time the execution of such a section will be faster than a section with a lower level. JIT - The compiler decides what compilation level to set for each program fragment based on how often that fragment is executed. Under the hood, the JVM uses 2 JIT compilers - C1 and C2. The C1 compiler is also called a client compiler and is capable of compiling code only up to the 3rd level. The C2 compiler is responsible for the 4th, most complex and fastest compilation level.
Compiling and executing Java applications under the hood - 3
From the above, we can conclude that for simple client applications, it is more profitable to use the C1 compiler, since in this case it is important for us how quickly the application starts. Server-side, long-lived applications can take longer to start, but in the future they must work and perform their function quickly - here the C2 compiler is suitable for us. When running a Java program on the x32 version of the JVM, we can manually specify which mode we want to use using the -clientand flags -server. When this flag is specified, -clientthe JVM will not perform complex bytecode optimizations, which will speed up the application startup time and reduce the amount of memory consumed. When specifying the flag, -serverthe application will take longer to start due to complex bytecode optimizations and will use more memory to store machine code, but the program will run faster in the future. In the x64 version of the JVM, the flag -clientis ignored and the application server configuration is used by default.

6. Conclusion

This concludes my brief overview of how compiling and executing a Java application works. Main points:
  1. The javac compiler converts a program's source code into bytecode that can be executed on any platform on which the Java virtual machine is installed;
  2. After compilation, the JVM interprets the resulting bytecode;
  3. To speed up Java applications, the JVM uses a Just-In-Time compilation mechanism that converts the most frequently executed sections of a program into machine code and stores them in memory.
I hope this article has helped you gain a deeper understanding of how our favorite programming language works. Thanks for reading, criticism is welcome!
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION