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

Compiling and running Java applications under the hood

Published in the Random EN group

Content:

  1. Introduction
  2. Compiling to bytecode
  3. An example of compiling and executing a program
  4. Executing a Program in a Virtual Machine
  5. Just-in-time (JIT) compilation
  6. Conclusion
Compiling and running 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 the written Java application. These days, there are all the trendy development environments that help you not think 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, interviews often ask questions about this topic, which is why I decided to write an article.

2. Compiling to bytecode

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

3. An example of compiling and executing a program

Suppose we have a simple program contained in a file Calculator.javathat takes 2 numerical 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);
    }
}
To compile this program into bytecode, use the compiler javacon the command line:
javac Calculator.java
After compilation, we get a file with bytecode at the output Calculator.class, which we can execute using the java machine installed on our computer with the java command on the command line:
java Calculator 1 2
Note that after the file name, 2 command line arguments were specified - the numbers 1 and 2. After the program is executed, 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 a package? Let's simulate this situation: create directories src/ru/codegymand place our class there. Now it looks like this (added the package name at the beginning of the file):
package ru.codegym;

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);
    }
}
Compile such a class with the following command:
javac -d bin src/ru/codegym/Calculator.java
In this example, we have used an additional compiler option -d binthat puts the compiled files into a directory binwith a structure similar to directory src, and 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 clarifying 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 start the program using the command:
java -classpath ./bin ru.codegym.Calculator 1 2
In this example, we needed to specify the fully qualified name of the class, including the name of the package in which it is located. The final file tree looks like this:
├── src
│     └── ru
│          └── codegym
│                  └── Calculator.java
└── bin
      └── ru
           └── codegym
                   └── Calculator.class

4. Program execution by a virtual machine

So, we launched the written program. But what happens when the compiled program is launched by the virtual machine? To begin with, 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 close to machine code. Interpretation is operator-by-operator (command-by-line, line-by-line) analysis, processing, and immediately executing the source program or query (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 the bytecode line by line into machine code and immediately executes it. Thus, when we run the compiled program, the virtual machine starts its interpretation, that is, the line-by-line conversion of the bytecode into machine code, as well as its execution. Unfortunately, pure bytecode interpretation is a rather long process and makes the java language slow compared to its competitors. In order to avoid this, a mechanism was introduced to speed up the interpretation of the bytecode by the virtual machine. This mechanism is called Just-in-time compilation (JITC).

5. Just-in-time (JIT) compilation

In simple words, the Just-In-Time compilation mechanism is as follows: if the program contains parts of the code that are executed many times, then they can be compiled once into machine code in order to speed up their execution in the future. After compiling such a part of the program into machine code, the next time this part of the program is called, the virtual machine will immediately execute the compiled machine code, and not interpret it, which will naturally speed up the execution of the program. The acceleration of the program is achieved by increasing the memory consumption (we need to store the compiled machine code somewhere!) and by increasing the time spent on compilation during the execution of the program. JIT compilation is a rather complicated mechanism, so let's go over the top. There are 4 levels of JIT compilation of bytecode to machine code in total. The higher the compilation level, the more difficult 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 independently decides which compilation level to set for each piece of program based on how often that piece is executed. Under the hood, the JVM uses 2 JIT compilers - C1 and C2. The C1 compiler is also called the client compiler and is only capable of compiling code up to level 3. The C2 compiler is responsible for the 4th, most complex and fastest level of compilation. Under the hood, the JVM uses 2 JIT compilers - C1 and C2. The C1 compiler is also called the client compiler and is only capable of compiling code up to level 3. The C2 compiler is responsible for the 4th, most complex and fastest level of compilation. Under the hood, the JVM uses 2 JIT compilers - C1 and C2. The C1 compiler is also called the client compiler and is only capable of compiling code up to level 3. The C2 compiler is responsible for the 4th, most complex and fastest level of compilation.
Compiling and running Java applications under the hood - 3
From the foregoing, 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, long-lived applications can start more time, 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 specified, -clientthe JVM will not perform complex bytecode optimizations, which will speed up the application start time and reduce the amount of memory consumed. When this flag is specified, -serverthe application will take longer to start due to complex bytecode optimizations and will use more memory to store machine code, however, such a program will run faster in the future. In the x64 version of the JVM, the flag -clientis ignored and the application's 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 that has the Java Virtual Machine installed;
  2. After compilation, the JVM interprets the resulting bytecode;
  3. To speed up Java applications, the JVM uses the Just-In-Time compilation mechanism, which converts the most frequently executed sections of the program into machine code and stores them in memory.
I hope this article has helped you understand more deeply 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