1. Overview
1.概述
The JVM is one of the oldest yet powerful virtual machines ever built.
JVM是有史以来最古老而强大的虚拟机之一。
In this article, we have a quick look at what it means to warm up a JVM and how to do it.
在这篇文章中,我们将快速了解一下预热JVM的含义以及如何预热。
2. JVM Architecture Basics
2.JVM架构基础
Whenever a new JVM process starts, all required classes are loaded into memory by an instance of the ClassLoader. This process takes place in three steps:
每当一个新的JVM进程启动时,所有需要的类都被ClassLoader的一个实例加载到内存中。这个过程分三步进行。
- Bootstrap Class Loading: The “Bootstrap Class Loader” loads Java code and essential Java classes such as java.lang.Object into memory. These loaded classes reside in JRE\lib\rt.jar.
- Extension Class Loading: The ExtClassLoader is responsible for loading all JAR files located at the java.ext.dirs path. In non-Maven or non-Gradle based applications, where a developer adds JARs manually, all those classes are loaded during this phase.
- Application Class Loading: The AppClassLoader loads all classes located in the application class path.
This initialization process is based on a lazy loading scheme.
这个初始化过程是基于一个懒惰的加载方案。
3. What Is Warming up the JVM
3.什么是预热JVM?
Once class-loading is complete, all important classes (used at the time of process start) are pushed into the JVM cache (native code) – which makes them accessible faster during runtime. Other classes are loaded on a per-request basis.
一旦类加载完成,所有重要的类(在进程启动时使用)都被推入JVM缓存(本地代码)–这使得它们在运行时可以更快地被访问。其他的类是在每个请求的基础上加载的。
The first request made to a Java web application is often substantially slower than the average response time during the lifetime of the process. This warm-up period can usually be attributed to lazy class loading and just-in-time compilation.
对Java网络应用程序提出的第一个请求往往比该进程生命周期内的平均响应时间要慢得多。这个预热期通常可以归因于懒惰的类加载和及时的编译。
Keeping this in mind, for low-latency applications, we need to cache all classes beforehand – so that they’re available instantly when accessed at runtime.
考虑到这一点,对于低延迟的应用,我们需要事先对所有的类进行缓存–这样它们在运行时就可以立即被访问。
This process of tuning the JVM is known as warming up.
这个调整JVM的过程被称为预热。。
4. Tiered Compilation
4.分层编排
Thanks to the sound architecture of the JVM, frequently used methods are loaded into the native cache during the application life-cycle.
由于JVM的健全架构,经常使用的方法在应用程序生命周期中被加载到本地缓存中。
We can make use of this property to force-load critical methods into the cache when an application starts. To that extent, we need to set a VM argument named Tiered Compilation:
我们可以利用这个属性,在应用程序启动时将关键方法强制加载到缓存中。为此,我们需要设置一个名为Tiered Compilation的VM参数:。
-XX:CompileThreshold -XX:TieredCompilation
Normally, the VM uses the interpreter to collect profiling information on methods that are fed into the compiler. In the tiered scheme, in addition to the interpreter, the client compiler is used to generate compiled versions of methods that collect profiling information about themselves.
通常情况下,虚拟机使用解释器来收集方法的剖析信息,并将其输入到编译器中。在分层方案中,除了解释器外,客户编译器还被用来生成收集自身剖析信息的方法的编译版本。
Since compiled code is substantially faster than interpreted code, the program executes with better performance during the profiling phase.
由于编译后的代码比解释后的代码要快得多,所以在剖析阶段,程序的执行性能更好。
Applications running on JBoss and JDK version 7 with this VM argument enabled tend to crash after some time due to a documented bug. The issue has been fixed in JDK version 8.
在 JBoss 和 JDK 第 7 版上运行的应用程序,如果启用该 VM 参数,往往会在一段时间后崩溃,这是由于记录的bug。这个问题在JDK版本8中已经被修复。
Another point to note here is that in order to force load, we’ve to make sure that all (or most) classes that are to going be executed need to be accessed. It’s similar to determining code coverage during unit testing. The more code is covered, the better the performance will be.
这里需要注意的另一点是,为了强制加载,我们必须确保所有(或大多数)将要执行的类都需要被访问。这类似于在单元测试中确定代码覆盖率。覆盖的代码越多,性能就越好。
The next section demonstrates how this can be implemented.
下一节展示了如何实现这一点。
5. Manual Implementation
5.手动实施
We may implement an alternate technique to warm up the JVM. In this case, a simple manual warm-up could include repeating the creation of different classes thousands of times as soon as the application starts.
我们可以实施另一种技术来预热JVM。在这种情况下,一个简单的手动预热可以包括在应用程序启动后立即重复创建不同的类,重复数千次。
Firstly, we need to create a dummy class with a normal method:
首先,我们需要创建一个具有正常方法的假类。
public class Dummy {
public void m() {
}
}
Next, we need to create a class that has a static method that will be executed at least 100000 times as soon as application starts and with each execution, it creates a new instance of the aforementioned dummy class we created earlier:
接下来,我们需要创建一个类,它有一个静态方法,一旦应用程序启动,它将被执行至少100000次,每执行一次,它就会创建一个我们先前创建的上述虚拟类的新实例。
public class ManualClassLoader {
protected static void load() {
for (int i = 0; i < 100000; i++) {
Dummy dummy = new Dummy();
dummy.m();
}
}
}
Now, in order to measure the performance gain, we need to create a main class. This class contains one static block that contains a direct call to the ManualClassLoader’s load() method.
现在,为了测量性能增益,我们需要创建一个主类。这个类包含一个静态块,它包含对ManualClassLoader的load()方法的直接调用。
Inside the main function, we make a call to the ManualClassLoader’s load() method once more and capture the system time in nanoseconds just before and after our function call. Finally, we subtract these times to get the actual execution time.
在main函数中,我们再次调用ManualClassLoader的load()方法,并捕捉我们函数调用前后的系统时间(纳秒)。最后,我们减去这些时间,得到实际执行时间。
We’ve to run the application twice; once with the load() method call inside the static block and once without this method call:
我们必须运行应用程序两次;一次是在静态块内调用load()方法,另一次是不调用这个方法。
public class MainApplication {
static {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Warm Up time : " + (end - start));
}
public static void main(String[] args) {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Total time taken : " + (end - start));
}
}
Below the results are reproduced in nanoseconds:
下面是以纳秒为单位再现的结果。
With Warm Up | No Warm Up | Difference(%) |
1220056 | 8903640 | 730 |
1083797 | 13609530 | 1256 |
1026025 | 9283837 | 905 |
1024047 | 7234871 | 706 |
868782 | 9146180 | 1053 |
As expected, with warm up approach shows much better performance than the normal one.
正如预期的那样,热身方法显示出比正常方法好得多的性能。
Of course, this is a very simplistic benchmark and only provides some surface-level insight into the impact of this technique. Also, it’s important to understand that, with a real-world application, we need to warm up with the typical code paths in the system.
当然,这是一个非常简单的基准,只提供了一些关于这个技术的影响的表面见解。此外,重要的是要理解,对于一个真实世界的应用,我们需要用系统中的典型代码路径进行预热。
6. Tools
6.工具
We can also use several tools to warm up the JVM. One of the most well-known tools is the Java Microbenchmark Harness, JMH. It’s generally used for micro-benchmarking. Once it is loaded, it repeatedly hits a code snippet and monitors the warm-up iteration cycle.
我们还可以使用一些工具来预热JVM。最著名的工具之一是Java Microbenchmark Harness,JMH>。它一般用于微型基准测试。一旦它被加载,它就会反复点击一个代码片断,并监控预热迭代周期。
To use it we need to add another dependency to the pom.xml:
为了使用它,我们需要在pom.xml中添加另一个依赖项。
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
We can check the latest version of JMH in Central Maven Repository.
我们可以在Central Maven Repository中查看JMH的最新版本。
Alternatively, we can use JMH’s maven plugin to generate a sample project:
另外,我们可以使用JMH的maven插件来生成一个示例项目。
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=com.baeldung \
-DartifactId=test \
-Dversion=1.0
Next, let’s create a main method:
接下来,让我们创建一个main方法。
public static void main(String[] args)
throws RunnerException, IOException {
Main.main(args);
}
Now, we need to create a method and annotate it with JMH’s @Benchmark annotation:
现在,我们需要创建一个方法,并用JMH的@Benchmark注解来注释它。
@Benchmark
public void init() {
//code snippet
}
Inside this init method, we need to write code that needs to be executed repeatedly in order to warm up.
在这个init方法里面,我们需要写一些需要反复执行的代码,以便进行预热。
7. Performance Benchmark
7.性能基准
In the last 20 years, most contributions to Java were related to the GC (Garbage Collector) and JIT (Just In Time Compiler). Almost all of the performance benchmarks found online are done on a JVM already running for some time. However,
在过去的20年里,对Java的大部分贡献都与GC(垃圾收集器)和JIT(及时编译器)有关。几乎所有在网上找到的性能基准都是在一个已经运行了一段时间的JVM上完成的。然而。
However, Beihang University has published a benchmark report taking into account JVM warm-up time. They used Hadoop and Spark based systems to process massive data:
然而,北京航空航天大学已经发表了一份考虑到JVM预热时间的基准报告。他们使用基于Hadoop和Spark的系统来处理大量数据。
Here HotTub designates the environment in which the JVM was warmed up.
这里HotTub指定了JVM被预热的环境。
As you can see, the speed-up can be significant, especially for relatively small read operations – which is why this data is interesting to consider.
正如你所看到的,速度的提高可以是显著的,特别是对于相对较小的读取操作–这就是为什么这个数据是值得考虑的。
8. Conclusion
8.结论
In this quick article, we showed how the JVM loads classes when an application starts and how we can warm up the JVM in order gain a performance boost.
在这篇快速文章中,我们展示了当应用程序启动时JVM是如何加载类的,以及我们如何预热JVM以获得性能提升。
This book goes over more information and guidelines on the topic if you want to continue.
这本书讲述了关于这个主题的更多信息和准则,如果你想继续的话。
And, like always, the full source code is available over on GitHub.
而且,像往常一样,完整的源代码可以在GitHub上获得。