Click on RoadToGrowth for the original article and more articles. Welcome to Star.
1. Relationship between JDK, JRE, and JVM
1.1 the JDK
The Java Development Kit (JDK) is a collection of software Development tools for developing Java applications. Includes Java runtime environment (JRE), interpreter (Java), compiler (JavAC), Java archive (JAR), document generator (Javadoc) and other tools. Simply put, if we want to develop Java programs, we need to install a version of the JDK toolkit.
1.2 the JRE
The Java Runtime Environment (JRE) provides an environment required by Java applications. The JRE consists of a Java Virtual machine (JVM), core classes, and supporting files. Simply put, if you want to run Java programs on a machine, you can either install the JDK or just install the JRE, which is relatively small.
1.3 the JVM
Java Virtual Machine(Java Virtual Machine) has three meanings:
JVM specification requirements
A concrete implementation (a computer program) that satisfies the JVM specification
A JVM instance is created when a Java command is written at a command prompt to run a Java class. This is what we mean when we remember only JVM; If we put on the name of some JVM, such as Zing JVM, it means the second one above
1.4 Relationship between JDK, JRE, and JVM
For scope, JDK > JRE > JVM:
- JDK = JRE + development tools
- JRE = JVM + class library
The development and running process of Java program is as follows:
We use the JDK (calling Java apis) to develop Java programs, compile them into bytecodes or package programs, and then use the JRE to start a JVM instance, load, validate, execute Java bytecodes and dependent libraries, and run Java programs.
The JVM, in turn, parses the Java bytecode of the program and its dependent libraries and executes it in native code, producing results.
1.5 What if I don’t know where the JDK installed automatically/by others is located?
The easiest/most troublesome way to query is to ask the person concerned.
There are many ways to find, such as tracing soft connections using which, whereis, ls ‐ L, or using the find command to find globally (sudo permissions may be required), such as:
- The JPS ‐ v
- whereis javac
- Ls ‐ l/usr/bin/javac
- The find / ‐ name javac
2. Common performance indicators
Without quantification there can be no improvement
- Analyze system performance issues: for example, whether our expected performance indicators are met, whether there are problems at the resource level, whether there are problems at the JVM level, whether there are problems at the key processing processes of the system, whether business processes need to be optimized
- Collect system status and logs through tools, including internal indicator collection, monitoring and obtaining key performance indicator data, as well as pressure measurement data and internal performance analysis data
- Adjust resource configuration based on analysis results and performance indicators, and continuously monitor and analyze the system to optimize the system performance until it meets system requirements and reaches the optimal system performance
2.1 Performance-related resources in computer systems are mainly divided into the following categories:
- CPU:CPU is the most critical computing resource in the system, which is limited in unit time and prone to bottlenecks due to unreasonable service logic processing. Wasting CPU resources and excessive CONSUMPTION of CPU resources are not ideal. We need to monitor related indicators.
- Memory: memory is corresponding application runtime can directly use the data of temporary space quickly, is also limited, use process over time, the application of memory and free memory, but the JVM GC to help us deal with these things, but if the GC configuration is not reasonable, will be the same after a certain amount of time, produce all sorts of problems, such as downtime, including OOM So memory metrics also need to be looked at;
- I/O(storage + Network): The CPU calculates the service logic in the memory. For long-term storage, the CPU must use disk storage media for persistence. In a multi-machine environment, distributed deployment, and external network service capability, many functions must be directly used by the network, and the I/O speed of the two components is slower than that of the CPU and memory. So that’s what we’re focusing on.
2.2 Common routines in performance optimization
Performance optimization usually involves bottlenecks, and bottlenecks follow the 80/20 rule. Even if we make a list of all the slower factors in the overall process and rank them in order of their impact on performance, the top 20% of bottlenecks will account for at least 80% of the performance impact. In other words, we solve the most important problems first, and the performance is better than half.
We usually start by looking at whether basic resources are a bottleneck. Depending on resources, plus configuration may be the fastest solution as long as the cost allows, and it may also be the most cost-effective and efficient solution. The system resources associated with the JVM are primarily CPU and memory. If a resource alarm or insufficiency occurs, evaluate the system capacity and analyze the cause.
Generally, there are three dimensions to measure system performance:
- Latency: Typically measures Response Time, such as average Response Time. However, sometimes the response time jitter is extremely severe, that is to say, the response time of some users is extremely high. In this case, we generally assume that we need to ensure that 95% of users respond within an acceptable range, so as to provide the majority of users with good user experience, which is the 95 line of delay (P95, Average response time of 95 out of 100 user requests), also 99 lines, maximum response time, etc. (95 lines and 99 lines are more common; When the number of users is large, any jitter on the network may cause the maximum response time to become very large. The maximum response time is not controllable and is generally not used.
- Throughput: Generally, we use Throughput per second (TPS) to measure Throughput for transaction type systems. For query search type systems, we can also use Throughput per second (QPS).
- Capacity: Also known as design Capacity, which can be understood as hardware configuration and cost constraints.
Performance indicators can also be divided into two categories:
- Service demand indicators: throughput (QPS, TPS), response time (RT), concurrency, success rate of services, etc.
- Resource constraint indicator: Resource consumption, such as CPU, memory, and I/O.
2.3 Performance Tuning Summary
The first step in performance tuning is to set metrics and collect data. The second step is to identify bottlenecks and then analyze and solve the bottlenecks. Using these methods, find the current performance limits. The limits of TPS and QPS, which are tuned to the point of no further optimization, are the limits. Knowing the limits allows us to measure flow and system stress in terms of business growth to plan capacity, prepare machine resources, and anticipate expansion plans. Finally, in the daily operation process of the system, continuous observation, gradually redo and adjust the above steps, long-term improvement and improvement of system performance.
We often say that “talking about performance without the scenario is hooliganism”. In the actual performance analysis and tuning process, we need to consider the cost and performance according to the specific business scenario, and use the most appropriate method to deal with it. Optimizing the system to 3000TPS doesn’t make sense to spend a few man-months optimizing the system to 3100TPS if you can already meet your business needs at a cost that’s affordable, nor does it make sense to double the cost optimizing the system to 5000TPS.
Donald Knuth once said that “premature optimization is the root of all evil” and that we need to consider optimizing systems at the right time. In the early days of the business, when volumes were small, performance was less important. When we build a new system, we should first consider whether the overall design is OK and whether the function implementation is OK, and then when the basic functions are almost done (of course, whether the overall framework meets the performance benchmark may need to be verified by POC(proof of concept) during the preparation stage of the project). And finally consider the performance optimization work. Because if you start thinking about optimization, you can overthink it and lead to overdesign. Moreover, before the completion of the main framework and functions, there may be relatively large changes. Once the optimization is made in advance, these changes may lead to the failure of the original optimization, and it is necessary to optimize again, making a lot of useless work.
3. JVM basics
3.1 Common programming language types
First, we can divide all kinds of programming from the bottom up into three basic categories: machine languages, assembly languages, and high-level languages.
According to the definition in the article “Development and Application of Computer Programming Language”, computer programming language can realize the communication and communication between man and machine, while computer programming language mainly includes assembly language, machine language and high-level language, the specific content is as follows:
- Machine language: this language mainly uses binary coding to send instructions, which can be quickly recognized by the computer. Its flexibility is relatively high, and the execution speed is considerable. There is a high similarity between machine language and assembly language, but due to its limitations, there are certain constraints in use.
- Assembly language: this language is mainly written in abbreviated English as a symbol, the use of assembly language to write are generally concise small programs, which is convenient in execution, but assembly language is more lengthy in the program, so it has a high error rate.
- High-level languages: the so-called high-level language, it is by a variety of programming languages after combining with the general, it can integrate the multiple instructions it into individual instructions to complete the transfer, its in the middle of the details operating instruction and obtained the appropriate simplification in such aspects as a routine, so, the whole program more simple, strong operability, and this kind of coding way to simplify, Make computer programming for the relevant staff professional level requirements relaxed.
3.2 Advanced language classification
-
High-level programming languages fall into two categories, based on the presence or absence of virtual machines:
-
There are virtual machines :Java, Lua, Ruby, partial JavaScript implementations, etc
-
No Virtual machines :C, C++, C#, Golang, and most common programming languages
-
If variables are divided by whether they have a definite type or can be arbitrarily varied, high-level programming languages can be divided into:
-
Static typing :Java, C, C++, etc
-
Dynamic typing: language for all script types
-
If the execution is compiled or interpreted, it can be divided into:
-
Compile and execute :C, C++, Golang, Rust, C#, Java, Scala, Clojure, Kotlin, Swift… , etc.
-
Explain execution :JavaScript partial implementation and NodeJS, Python, Perl, Ruby… , etc.
-
In addition, we can also be classified by language characteristics:
-
Process-oriented :C, Basic, Pascal, Fortran, etc
-
Object oriented :C++, Java, Ruby, Smalltalk, etc
-
Functional programming :LISP, Haskell, Erlang, OCaml, Clojure, F#, etc
Some can even be classified as pure object-oriented languages, such as Ruby, where everything is an object. (Not everything in Java is an object. Primitives such as Int, long, etc., are not objects, but their wrapper classes Integer, Long are objects.) There are also languages that can be used as both compiled and scripting languages, such as Groovy.
3.3 About Cross-platform
Now cross-platform, why cross-platform, because we want to write code and programs that, at the source code level or when compiled, can run on multiple platforms, without having to implement two sets of code for different points on each platform. Typically, we write a Web application and want to deploy it on Windows, Linux, or even MacOS. This is the ability to cross platform, greatly reducing development and maintenance costs, and has won praise in the commercial market.
In this way, interpreted languages are generally cross-platform, and the same script can be interpreted and executed by interpreters on different platforms. But for compiled languages, there are two levels of cross-platform: source cross-platform and binary cross-platform.
Typical cross-platform source code (C++):
Typical binary cross-platform (Java bytecode):
As you can see, in C++ we need to compile the source code separately on different platforms, generate the platform-specific binary executable file, and then run on the corresponding platform. This requires development tools and compilers to be available across platforms, and development libraries to rely on across platforms need to be consistent or compatible. This was so painful in the past that it was nicknamed “dependency hell.” The slogan of C++ is “write once, compile everywhere”, but in reality it is “write once, debug everywhere, find dependencies everywhere, change configuration”. As you can imagine, when you compile a code and find dozens of dependencies missing, you can’t find them everywhere, or when you find them, they are not compatible with the local version. This is a desperate thing.
The Java language solves this problem first through virtual machine technology. The source code is compiled once, then the compiled class files or JAR packages are deployed to different platforms and can be executed directly on the JVMS installed on those systems. You can also copy dependent libraries (JAR files) to the target machine, and gradually you have Maven central libraries that can be used directly on all platforms (similar to yum or Aptget sources on Linux, Homebrew on MacOS, Modern programming languages generally have this package dependency management mechanism: Python’s PIP, Dotnet’s Nuget, NodeJS’s NPM, Golang’s DEP, Rust’s Cargo, etc.). This enables the ability for the same application to run directly on different platforms.
To summarize cross-platform:
- Scripting languages are directly executed using interpreters from different platforms, called script cross-platform, and the differences between platforms are resolved by the interpreters on different platforms. This code is generic, but requires interpretation and translation, which is inefficient.
- The code of compiled language is cross-platform. The same code needs to be compiled into the corresponding binary file by the compiler of different platforms, and then distributed and executed. The differences between different platforms are solved by the compiler. The compiled files are directly executable instructions for the platform and run efficiently. However, building complex software on different platforms and relying on configuration can cause many environmental problems, resulting in high development and maintenance costs.
- Compiled binary cross-platform language, the same code, compiled into a generic binary file first, and then distributed to different platforms, the virtual machine runtime loading and execution, so that it will be integrated the advantage of two other cross-platform language, convenient and quick to run on a variety of platforms, while the efficiency may be compared to the local language of the compiled classes should be slightly lower. These are also the advantages and disadvantages of the Java virtual machine.
3.4 About Runtime and VM
We have mentioned the Java runtime and THE JVM many times before, but the JRE is simply the Java runtime, including the virtual machine and associated libraries and other resources. It can be said that the runtime provides the basic environment for the program to run. The JVM needs to load all the runtime core libraries and other resources at startup, and then load our application bytecode, so that the application bytecode can run in the JVM container.
However, there are also some languages that do not have virtual machines, and rely on core libraries and other features to be supported when compiled and packaged, either statically packaged or dynamically linked into the application, such as Golang and Rust, C#, etc. The runtime is then combined with program instructions to form a complete application, with the benefit of eliminating the need for a virtual machine environment and the disadvantage of making the compiled binaries no longer directly cross-platform.
3.5 About Memory Management and Garbage Collection (GC)
Memory management is the life cycle management of memory, including memory application, compression, and reclamation. Java’s memory management is called GC, and the JVM’s GC module not only manages the collection of memory, but also allocates and compacts it.
4. Java bytecode
Bytecode in Java, called bytecode, is an intermediate code format for compiled Java code. The JVM needs to read and parse the bytecode to perform the task. It consists of one-byte instructions and theoretically supports a maximum of 256 opcodes. In fact, Java only uses about 200 opcodes, with some remaining opcodes reserved for debugging operations.
An opcode, called an instruction, consists of a type prefix and an operation name.
For example, the ‘I’ prefix stands for ‘integer’, so ‘iadd’ is easy to understand and means to add integers.
4.1 According to the nature of instructions, they can be divided into four categories:
- Stack manipulation instructions, including instructions to interact with local variables
- Program flow control instructions
- Object manipulation instructions, including method invocation instructions
- Arithmetic operations and type conversion instructions
There are also instructions that perform specialized tasks, such as the synchronization instruction and the exception throwing instruction
4.2 Object initialization Instructions: Introduction to new instructions, init, and Clinit
We all know that new is a keyword in the Java programming language, but in bytecode, there is also an instruction called new. When we create an instance of the class, the compiler generates an opcode like the following:
```
0: new #2 // class demo/jvm0104/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
```
Copy the code
When you see the new, DUP and Invokespecial directives together, you must be creating an instance object of the class! Why three instructions instead of one? This is because:
- The new directive just creates the object, but does not call the constructor.
- The invokespecial directive is used to call a particular method, of course the constructor.
- The DUP directive is used to copy the value at the top of the stack.
- Since the constructor call does not return a value, without the DUP instruction, after the method is called on the object and initialized, the operand stack will be empty, causing a problem after initialization, and the following code will not be able to process it.
When the constructor is called, another similar method is executed, even before the constructor is executed. Another method that can be executed is the static initializer of the class, which cannot be called directly but is triggered by the instructions: new, getstatic, putstatic or Invokestatic.
4.3 Stack memory operation instructions
There are many instructions to manipulate the method stack. Some basic stack instructions were also mentioned earlier: they push values onto the stack or get values from the stack. In addition to these basic operations, there are also instructions that manipulate stack memory; For example, the swap directive swaps the values of the top two elements on the stack. Here are some examples:
The most basic are the DUP and POP commands.
- The DUP instruction copies the value of the top element on the stack.
- The POP instruction removes the topmost value from the stack.
There are also more complicated instructions: swap, dup_x1, and dup2_x1, for example.
- As the name suggests, the swap instruction swaps the values of two elements at the top of the stack, such as A and B switching places (example 4 in figure 4);
- Dup_x1 copies the value of the top element on the stack after inserting it into the top two values (example 5 in figure);
- Dup2_x1 copies the values of the top two elements of the stack and inserts the top three values (example 6 in the figure).
Dup, dup_x1, dup2_x1
- Dup instruction: Copy the top value of the stack and push the copied value onto the stack.
- Dup_x1 instruction: Copy the top value of the stack and insert it below the top two values.
- Dup2_x1 instruction: Copy one 64-bit or two 32-bit values at the top of the stack and insert the copied values, in the original order, below the 32-bit values below the original values.
Arithmetic operation instruction and type conversion instruction
There are many instructions in Java bytecode that can perform arithmetic operations. In fact, a large part of the representation of the instruction set is all about mathematical operations. Add, subtract, multiply, divide, and reverse instructions for all numeric types (int, long, double, float). What about byte and char Boolean? The JVM is treated as an int. There are also instructions for converting between data types.
A conversion occurs when we want to assign a value of type int to a variable of type long.
6. Method call instruction and parameter passing
- Invokestatic, as its name suggests, is used to invokestatic methods of a class, and is one of the fastest method invocation directives.
- Invokespecial, as we have learned, is used to call constructors, but can also be used to call private methods in the same class, as well as visible superclass methods.
- Invokevirtual, for specific types of target objects, invokevirtual is used to call public, protected, and packaged private methods.
- Invokeinterface, the InvokeInterface directive is used when the method to be invoked belongs to an interface.
So what’s the difference between Invokevirtual and InvokeInterface? That’s a really good question. Why do you need invokevirtual and InvokeInterface? After all, all interface methods are public, so why not just use Invokevirtual? This is due to optimization of method calls. The JVM must parse the method before it can be called
- With the Invokestatic directive, the JVM knows exactly which method to invoke: because it is a static method, it can only belong to one class.
- With Invokespecial, the number of lookups is also small and parsing is easier, so the runtime can find the desired method faster.
- Before JDK7, the byte code instruction set of ava virtual machine had only the four instructions mentioned above (Invokestatic, Invokespecial, Invokevirtual, invokeInterface). With the release of JDK 7, an Invokedynamic instruction was added to the bytecode instruction set. This newly added instruction is one of the improvements made to implement support for “Dynamically Typed languages,” and is the basis for implementing lambda expressions supported by JDK 8 onwards.
7. Java class loader
7.1 Class life cycle and loading process
The life cycle of a class in the JVM has seven phases, 2. Loading, Verification, Preparation, Resolution, Initialization, Using and Unloading are two common problems. The first five parts (load, validate, prepare, parse, initialize) are collectively referred to as class loading. Let’s break them down.
7.1.1 load
The loading phase can also be called the “load” phase. The main operation in this phase is to get the byte stream in binary Classfile format based on the fully qualified name of the class that is clearly known. In short, it is to find the “classfile” in the/JAR package of the file system/or wherever it exists. If the binary representation is not found, a NoClassDefFound error is thrown. The load phase does not check the syntax and format of the classfile. The entire process of class loading is mainly done by the JVM and the Java class loading system, of course, the specific loading stage is completed by the JVM and a specific classLoader (java.lang.classloader).
7.1.2 check
The first stage of the linking process is validation to ensure that the byte stream information in the class file meets the requirements of the current virtual machine and does not compromise the virtual machine security. The validation process checks the semantics of classfile, determines symbols in the constant pool, and performs type checking, primarily to determine the validity of bytecode, such as magic number, to verify version numbers. These tests may be thrown in the process of VerifyError, ClassFormatError or UnsupportedClassVersionError. Because the validation genus of classfile is part of the linking phase, other classes may need to be loaded during this process, and during the loading of a class, the JVM must load all of its superclasses and interfaces. If there is a problem with the class hierarchy (for example, the class is its own superclass or interface and has an infinite loop), the JVM will throw a ClassCircularityError. And if the implementation of the interface is not an interface, or a statement of the superclass is an interface, also sell IncompatibleClassChangeError.
7.1.3 preparation
It then enters the preparation phase, which creates static fields and initializes them to standard default values (such as NULL or 0 values), and allocates the method table, which allocates the memory space used by these variables in the method area. Note that no Java code was executed during the preparation phase.
Such as:
public static int i = 1;
Copy the code
The value of I is initialized to 0 in the preparation phase, and assignment to 1 is performed later in the class initialization phase; However, some JVMS behave differently if final is used as a static constant:
public static final int i = 1;
Copy the code
For example, other languages (C#) have a direct const keyword, which makes it easier to tell the compiler to replace it with a constant at compile time, similar to a macro instruction.
7.1.4 parsing
It then enters the optional parsing symbol reference phase. Parsing constant pool, there are four main types: class or interface parsing, field parsing, class method parsing, interface method parsing.
To put it simply, when we write code that references a variable to an object, the reference is stored as a symbolic reference in the.class file (equivalent to making an index record). In the parsing phase, it needs to be parsed and linked as a direct reference (equivalent to pointing to the actual object). If there is a direct reference, the target of the reference must exist in the heap. When you load a class, you need to load all the super classes and super interfaces.
7.1.5 initialization
The JVM specification explicitly states that class initialization must be performed on the first “active use” of a class. The initialization process includes executing:
- Class constructor method
- Static Static variable assignment statement
- Static Static code block
If a subclass is initialized, its parent class is initialized first, ensuring that the parent class is initialized before the subclass. So to initialize a class in Java, you must first initialize java.lang.Object, because all Java classes inherit from java.lang.Object.
7.2 Class loading time
Now that we know how the class loads, when does initialization of the class get triggered? The JVM specification enumerates the following triggers:
- When the VM starts, it initializes the main class specified by the user, which is the class where the main method is started.
- When a new directive is encountered to create a new instance of the target class, the target class of the new directive is initialized when a new class is created
- When an instruction that calls a static method is encountered, initialize the class of the static method;
- When an instruction is encountered that accesses a static field, initialize the class in which the static field resides.
- Initialization of a subclass triggers initialization of its parent class;
- If an interface defines the default method, the initialization of the class that implements the interface directly or indirectly triggers the initialization of the interface.
- When a reflection call is made to a class using the reflection API, it initializes the class. As before, the reflection call is either instantiated or static.
- The first time you call a MethodHandle instance, you initialize the class to which the method points.
Class initialization is not performed in the following cases:
- A reference to a static field of a parent class by a subclass triggers initialization of the parent class, not the subclass.
- Defining an array of objects does not trigger initialization of the class.
- Constants are stored in the constant pool of the calling class at compile time. There is no direct reference to the class in which the constant is defined, and the class in which the constant is defined is not triggered.
- Getting a Class object by its name does not trigger Class initialization. Hello. Class does not initialize the Hello Class.
- If initialize is false, Class initialization is not triggered when loading a Class using class.forname. This parameter tells the vm whether to initialize the Class. Class.forname (” JVM.hello “) loads the Hello Class by default.
- The default loadClass method of ClassLoader also does not trigger the initialization action (loaded, but not initialized).
7.3 Class Loading Mechanism
The Class loading process can be described as “obtaining the Class object describing a Class by the fully qualified name of a Class A.B.C. XClass”. This process is accomplished by the “ClassLoader”. The advantage of this is that the subclass loader can reuse classes loaded by the parent loader. Class loaders of the system are divided into three types:
- Start the class loader (BootstrapClassLoader)
Bootstrap class loader: it is used to load Java core classes. It is implemented in native C++ code and does not inherit from
Java.lang.ClassLoader(responsible for loading all classes in the JDK jre/lib/rt.jar). It can be viewed as native to the JVM and not directly available at the code level
Starts a reference to the classloader, so it is not allowed to operate on it directly, and will be null if printed. For example, java.lang.String is loaded by the startup class loader
, so the String. Class. GetClassLoader () will return null. But you’ll see later that you can influence what it loads with command-line arguments.
-
Extended class loader (ExtClassLoader)
-
Extensions Class Loader: this is responsible for loading the JRE extension directory, lib/ext or JAR classes in the directory specified by the java.ext.dirs system property. The parent class loader that gets it directly in the code is null(because you can’t get the boot class loader).
-
Application Classloader
-
App Class Loader: This is responsible for loading the classpath or cp option from Java commands, the JAR packages specified by the java.class.path system property, and the classpath at JVM startup. The application ClassLoader can be obtained in the application code through the static method getSystemClassLoader() of the ClassLoader. If not specified, user-defined classes are loaded from the custom class loader if no custom class loader is used.
The class loading mechanism has three characteristics:
- Parent delegate: When a custom class loader needs to load a class, such as java.lang.String, it is lazy and does not try to load it directly. Instead, it delegates its parent to load it first. For example, if the launcher class loader has already loaded a class such as java.lang.String, all child loaders do not need to load it themselves. If none of the class loaders has loaded the class with the specified name, ClassNotFountException is thrown.
- Responsible dependencies: If a loader loads a class and finds that the class depends on several other classes or interfaces, it will try to load those dependencies as well.
- Cache loading: To improve loading efficiency and eliminate double loading, once a class is loaded by a class loader, it caches the load result and does not load again.