jvm与jit编译器的区别_了解jvm和jit编译器的第3部分

jvm与jit编译器的区别

Hello people!

大家好!

Following the thinking line based on the second part of this series, I’ll explain better what means deoptimization.

按照本系列第二部分的思路,我将更好地解释什么是去优化

Deoptimization means that the compiler needs to “undo” a previous compilation. The effect is that the performance of the application will be reduced (at least until the compiler can recompile the code).

反优化意味着编译器需要“撤消”先前的编译。 结果是将降低应用程序的性能(至少在编译器可以重新编译代码之前)。

So to clarify, when the JVM executes code, it does not begin compiling the code immediately, and here are two basic reasons for this.

因此需要澄清的是,当JVM执行代码时,它不会立即开始编译代码,这是两个基本原因。

First one: Let’s imagine that we have a code that will be executed only one time, then compiling it is a completely wasted effort. It will be faster to just interpret the Java bytecode.

第一个:假设我们有一个执行一次的代码,然后编译它是完全浪费的工作。 仅解释Java字节码会更快。

But if the code in question is a frequently called method or a loop that runs many times, then compiling it will bring benefits. The effort required to compile that code will be offset by the savings in several executions of the compiled code more quickly.

但是,如果所讨论的代码是经常调用的方法或运行多次的循环,那么对其进行编译将带来好处。 编译该代码所需的工作将被更快地节省多次执行编译后的代码所抵消。

That trade-off is one reason that the compiler executes the interpreted code first.

这种折衷是编译器首先执行解释后的代码的原因之一。

The second reason is one of optimization: the more times that the JVM executes a particular method or loop, the more information it has about that code (is it also called “profiling”). This information allows the JVM to make a lot of optimizations when compiling the code.

第二个原因是优化:JVM执行特定方法或循环的次数越多,它获得的有关该代码的信息就越多(也称为“分析”) 。 此信息使JVM在编译代码时可以进行很多优化。

According to Scott Oaks, for a simple example, let’s consider the equals() method. This method exists in every Java object (because it is inherited from the Object class) and is often overridden. When the interpreter encounters the statement b = obj1.equals(obj2), it must look up the type (class) of obj1 in order to know which equals() method to execute. This dynamic lookup can be somewhat time-consuming. Over time, say the JVM notices that each time this statement is executed, obj1 is of type java.lang.String. Then the JVM can produce a compiled code that directly calls the String.equals() method. Now the code is faster not only because it is compiled but also because it can skip the lookup of which method to call. It’s not quite as simple as that; it is possible the next time the code is executed that obj1 refers to something other than a String. The JVM will create a compiled code that deals with that possibility, which will involve deoptimizing and then reoptimizing the code in question. Nonetheless, the overall compiled code here will be faster (at least as long as obj1 continues to refer to a String) because it skips the lookup of which method to execute. That kind of optimization can be made only after running the code for a while and observing what it does.

根据斯科特·奥克斯(Scott Oaks)的说法,举一个简单的例子,我们考虑equals()方法。 该方法存在于每个Java对象中(因为它是从Object类继承的),并且经常被覆盖。 当解释器遇到语句b = obj1.equals(obj2)时 ,它必须查找obj1的类型(类)才能知道要执行哪个equals()方法。 这种动态查找可能会比较耗时。 随着时间的流逝,例如JVM注意到,每次执行此语句时, obj1的类型都是java.lang.String 。 然后,JVM可以生成直接调用String.equals()方法的编译代码。 现在,代码变得更快,不仅因为它已编译,而且因为它可以跳过对要调用的方法的查找。 这不是那么简单。 下次执行代码时, obj1可能引用的不是String 。 JVM将创建一个处理该可能性的已编译代码,这将涉及先对代码进行优化 ,然后再对其进行优化 。 尽管如此,这里的整体编译代码会更快(至少只要obj1继续引用String ),因为它会跳过要执行的方法的查找。 只有在运行代码一段时间并观察其作用之后,才能进行这种优化。

This is the second reason JIT compilers wait to compile sections of code.

这是JIT编译器等待编译代码段的第二个原因。

Now let’s talk a little bit more about Deoptimization.

现在,让我们再谈谈反优化

Deoptimization can happen in two cases, when code is made not entrant and when code is made zombie, as mentioned before.

如前所述,在两种情况下可能会发生非优化:代码不准进入和代码僵尸

Not entrant code

非参赛者代码

Two things can cause code to be made not entrant. The first one is when we are using polymorphism and interfaces, and the second one can simply happen during the tiered compilation (from 1 to 4).

有两件事可能导致代码无法进入。 第一个是当我们使用多态和接口时,第二个可以简单地发生在分层编译期间(从1到4)

So, let’s create 1 interface with 2 implementations, it will help us to understand better how it works.

因此,让我们创建具有2个实现的1个接口,它将帮助我们更好地了解其工作方式。

Interface: MyInterface

界面: MyInterface

Implementation: MyInterfaceImpl and MyInterfaceLoggerImpl

实现: MyInterfaceImplMyInterfaceLoggerImpl

Let’s look at the first case.

让我们看一下第一种情况。

MyInterfaceImpl is a simple implementation but the MyInterfaceLoggerImpl is adding some log statements on the implemented method.

MyInterfaceImpl是一个简单的实现,但是MyInterfaceLoggerImpl在实现的方法上添加了一些日志语句。

Now, let’s imagine that the first 45.000 executions will call the MyInterfaceImpl, and then the MyInterfaceLoggerImpl will be called for the rest 5.000 executions.

现在,让我们想象一下,第一45.000执行将调用MyInterfaceImpl,然后MyInterfaceLoggerImpl将被调用 其余的5.000次执行。

package main;


public class DeoptimizationExample {
    public static void main(String[] args) {
        for (int i = 0; i < 50000; i++) {
            MyInterface myInterface;
            if (i < 45000) {
                // The first 45.000 executions will enter here
                myInterface = new MyInterfaceImpl();
            } else {
                myInterface = new MyInterfaceLoggerImpl();
            }
            myInterface.addARandomNumber(50);
        }
    }
}


interface MyInterface {
    void addARandomNumber(double value);
}


class MyInterfaceImpl implements MyInterface {
    @Override
    public void addARandomNumber(double value) {
        double random = Math.random();
        double finalResult = random + value;
    }
}


class MyInterfaceLoggerImpl implements MyInterface {
    @Override
    public void addARandomNumber(double value) {
        System.out.println("The value is: " + Math.random() + value);
    }
}

If you execute the code above using the flag -XX:+PrintCompilation you will see this line:

如果使用标志-XX:+ PrintCompilation执行上述代码,您将看到以下行:

152  184       3       main.MyInterfaceImpl::addARandomNumber (10 bytes)   made not entrant

And the explanation is because the compiler will see that the current type of the myInterface object is MyInterfaceImpl. It will then inline code and perform other optimizations based on this knowledge.

解释是因为编译器会看到myInterface对象的当前类型是MyInterfaceImpl 。 然后它将内联代码并根据此知识执行其他优化。

Now after a bunch of executions (45.000 based on our example) using the MyInterfaceImpl, we enter another scenario, where the implementation will be MyInterfaceLoggerImpl. Now the assumption the compiler made regarding the type of the myInterface object is wrong, the previous optimizations are invalid. It will generate a deoptimization, and the previous optimizations are discarded. If a lot of additional calls are made using the MyInterfaceLoggerImpl, the JVM will quickly compile that code and make new optimizations.

现在,在使用MyInterfaceImpl进行一堆执行(基于我们的示例为45.000)之后,我们进入另一个场景,其中的实现将是MyInterfaceLoggerImpl 。 现在,假设编译器对myInterface对象的类型做出了错误的假设,则先前的优化无效 。 它将生成去优化 ,并且先前的优化将被丢弃 。 如果使用MyInterfaceLoggerImpl进行了许多其他调用,则JVM将快速编译该代码并进行新的优化。

So, now we already understand how the first scenario (polymorphism and interface) works, let’s talk about the second scenario.

因此,现在我们已经了解了第一种情况( 多态性和interface )如何工作,让我们来讨论第二种情况。

The second thing that can cause code to be made not entrant is because of how tiered compilation works. When a code is compiled by the C2 (Server) compiler (to a tier 4), the JVM must replace the code already compiled by the C1 (Client) compiler. It does this by marking the old code as not entrant and using the same deoptimization mechanism to replace the marked code for the newly compiled (and more efficient) code. So, when a program is running with tiered compilation, the compilation log will show some methods that are not entrant. But in this case, this “deoptimization” is, in fact, making the code that much faster.

导致代码无法进入的第二件事是由于分层编译的工作原理。 当C2(服务器)编译器将代码编译到第4层时,JVM必须替换C1(客户端)编译器已经编译的代码。 它通过将旧代码标记为不可进入并使用相同的去优化机制来将标记的代码替换为新编译(且效率更高)的代码来实现。 因此,当程序运行分层编译时,编译日志将显示一些不可重入的方法。 但是在这种情况下,这种“非优化”实际上使代码变得更快。

Deoptimizing zombie code

优化僵尸代码

According to Scott Oaks, when the compilation log reports that it has made zombie code, it is saying that it has reclaimed previous code that was made not entrant.

据斯科特·奥克斯(Scott Oaks)称,当编译日志报告它已经制作了僵尸代码时,就意味着它已经收回了以前没有进入的代码。

Using our example, after a test running with the MyInterfaceLoggerImpl implementation, the code for the MyInterfaceImpl class was made not entrant. But objects of the MyInterfaceImpl class were still in memory. Eventually, all those objects will be collected by Garbage Collector (GC). When that happened, the compiler noticed that the methods of that class will be marked as zombie code.

使用我们的示例,在使用MyInterfaceLoggerImpl实现运行测试之后,将MyInterfaceImpl类的代码设为 entrant 。 但是MyInterfaceImpl类的对象仍在内存中。 最终,所有这些对象将由垃圾收集器(GC)收集。 发生这种情况时,编译器注意到该类的方法将被标记为僵尸代码

Thinking in performance, this is a good thing and I’ll explain why.

考虑性能,这是一件好事,我将解释原因。

That compiled code is kept in a fixed-size code cache. When zombie methods are identified, this code can be removed from the code cache, making room for other code to be compiled and added there.

该编译后的代码将保存在固定大小的代码缓存中。 识别出僵尸方法后,可以从代码缓存中删除此代码,从而为其他代码的编译和添加留出空间。

Amazing Julio, but let’s imagine that I want to know more about my compilation than the info that -XX:+PrintCompilation provides to me.

令人惊叹的Julio,但让我们想象一下,除了-XX:+ PrintCompilation提供给我的信息之外,我还想对我的编译了解更多

It is totally possible my dear friend!

我亲爱的朋友完全有可能!

There are 2 flags that will help us to generate a file that provides us more information and these flags are -XX:+UnlockDiagnosticsVMOptions and -XX:+LogCompilation.

有2个标志可以帮助我们生成一个提供更多信息的文件,这些标志是-XX:+ UnlockDiagnosticsVMOptions-XX:+ LogCompilation

It will create a file called hotspot_pid<SOME_PID>.log and you can open this file and see a lot of info around your application.

它将创建一个名为hotspot_pid <SOME_PID> .log的文件,您可以打开此文件并查看有关应用程序的很多信息。

By default, this file will be placed in the same folder of your project, but you can change the location of the file, using the flag -XX:LogFile=<PATH>.

默认情况下,此文件将放置在项目的同一文件夹中,但是您可以使用标志-XX:LogFile = <PATH>更改文件的位置。

But Julio, this log is really hard to understand, is there any way to make it easier to read?

但是Julio这个日志真的很难理解,有什么方法可以使它更容易阅读?

Of course, it is, the Java community is amazing!

当然,Java社区很棒!

There is an amazing open-source project called JITWatch, maintained by AdoptOpenJDK, that will help us to track our JIT Compiler.

AdoptOpenJDK维护了一个名为JITWatch的令人惊叹的开源项目,该项目将帮助我们跟踪JIT编译器。

To use that is really simple, just clone the official repository and run using Maven or Gradle, like the example below.

要使用它真的很简单,只需克隆 官方存储库并使用MavenGradle运行,如下例所示。

git clone git@github.com:AdoptOpenJDK/jitwatch.git
cd jitwatch
mvn clean compile test exec:java
# or
gradlew clean build run

Important note: To generate the hotspot.log file used by JITWatch, we need one more flag, and it is “-XX:+TraceClassLoading”. So, in total, you need those 3 flags to generate a hotspot.log that JITWatch can understand:

重要说明:要生成JITWatch使用的hotspot.log文件,我们还需要一个标志,它是“ -XX:+ TraceClassLoading ”。 因此,总的来说,您需要使用这3个标记来生成JITWatch可以理解的hotspot.log:

-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+LogCompilation

Once running, you can open your log file (hotspot_pid<SOME_PID>.log) there and click on the “Start” button, and then you will see all the analysis around your HotSpot log file, like the image below.

运行后,您可以在此处打开日志文件( hotspot_pid <SOME_PID> .log ),然后单击“ 开始 ”按钮,然后您将看到HotSpot日志文件周围的所有分析结果,如下图所示。

Image for post

To know more about this fantastic tool visit his Wiki page clicking here.

要了解有关此出色工具的更多信息,请访问他的Wiki页面,单击此处

Now let’s imagine that, based on our analysis, we decided that we need to increase the Code Cache size, how can we do that?

现在让我们想象,根据我们的分析,我们决定需要增加代码缓存的大小 ,我们该怎么做?

I’ll explain how to do that in the next article of this series!

我将在本系列的下一篇文章中解释如何做到这一点!

Again, thank you very much for your time, I hope that you are enjoying this series!

再次感谢您的宝贵时间,希望您喜欢这个系列!

See you in the next article, part four!

下一篇,第四部分见!

翻译自: https://medium.com/@julio.falbo/understand-jvm-and-jit-compiler-part-3-556d3f21df9f

jvm与jit编译器的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值