了解jvm和jit编译器的第2部分

Hello again, people!

大家好!

If you read the first part of this series and is wondering:

如果您阅读了本系列的第一部分 ,并且想知道:

How can I check if that specific code was compiled to a native machine language?

如何检查特定代码是否已编译为本地机器语言?

You are in the correct place! Now I’ll explain we can check and analyze the JIT Compilation logs. So, let’s go!

您来对地方了! 现在,我将解释我们可以检查和分析JIT编译日志。 所以,走吧!

There is a JVM flag that can help us with this investigation, and this flag is “-XX:+PrintCompilation”.

有一个JVM标志可以帮助我们进行调查,该标志是“ -XX:+ PrintCompilation ”。

Here is an example of the output:

这是输出示例:

50    1       3       java.lang.StringLatin1::hashCode (42 bytes)53    2       3       java.lang.Object::<init> (1 bytes)53    3       3       java.lang.String::isLatin1 (19 bytes)54    4       3       java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)
60 5 3 java.lang.String::charAt (25 bytes)
60 6 3 java.lang.StringLatin1::charAt (28 bytes)
60 7 3 java.lang.String::coder (15 bytes)

88 40 n 0 java.lang.invoke.MethodHandle::linkToSpecial(LLLLLLLL)L (native) (static)
88 39 ! 3 java.util.concurrent.ConcurrentHashMap::putVal (432 bytes)
90 41 n 0 java.lang.System::arraycopy (native) (static)
91 42 3 java.lang.String::length (11 bytes)
...
129 3 3 java.lang.String::isLatin1 (19 bytes) made not entrant
...
138 150 n 0 java.lang.Object::getClass (native)

Most lines of the compilation log have the following format:

编译日志的大多数行具有以下格式:

timestamp compilation_id attributes tiered_level method_name size deopt

The timestamp (milliseconds) here is the time after the compilation has finished (relative to 0, which is when the JVM started).

此处的时间戳 (毫秒)是编译完成后的时间(相对于JVM启动时的0)。

The compilation_id is an internal task identifier. Usually, this number will simply increase monotonically, but sometimes you can see those out of sync. This happens, normally, when we have multiple compilation threads and means that those compilation threads are running out of order (faster or slower). So, it is just a function of thread scheduling.

Compilation_id是一个内部任务标识符。 通常,此数字只会单调增加,但有时您会看到不同步的数字。 通常,当我们有多个编译线程时,就会发生这种情况,这意味着这些编译线程的运行顺序将变乱(更快或更慢)。 因此,这只是线程调度的功能。

The attributes field is a string with five characters that indicate the state of the code that is being compiled. If a specific attribute applies to the given compilation, the character for that specific attribute will be printed, otherwise, blank will be printed. The characters are:

属性字段是一个包含五个字符的字符串,用于指示正​​在编译的代码的状态。 如果特定属性适用于给定的编译,则将打印该特定属性的字符,否则将打印空白。 字符是:

% - The compilation is OSR (on-stack replacement).s - The method is synchronized.! - The method has an exception handler.b - Compilation occurred in blocking mode.n - Compilation occurred for a wrapper to a native method.

The first of these attributes (%) refers to on-stack replacement (OSR). We need to remember that JIT compilation is an asynchronous process. So, when the JVM decides that a certain block of code should be compiled, that block of code is put in a queue*. Instead of waiting for the compilation, the JVM will continue interpreting the method, and the next time the method is called, the JVM will execute the compiled version of the method (assuming the compilation has finished).

这些属性中的第一个( )是指堆栈上的替换 ( OSR )。 我们需要记住,JIT编译是一个异步过程。 因此,当JVM决定应编译某个代码块时,该代码块将被放入队列*。 JVM将继续解释该方法,而不是等待编译,并且在下次调用该方法时,JVM将执行该方法的已编译版本(假设编译已完成)。

Now let’s consider a long-running loop, according to Scott Oaks, the JVM will notice that the loop itself should be compiled and will queue* that code for compilation. But that isn’t sufficient, the JVM has to have the ability to start executing the compiled version of the loop while the loop is still running — it would be inefficient to wait until the loop and enclosing method exit (which may not even happen). Hence, when the code for the loop has finished compiling, the JVM replaces the code (on the stack), and the next iteration of the loop will execute the much faster-compiled version of the code.

现在,让我们考虑一个长时间运行的循环,根据Scott Oaks的说法,JVM将注意到该循环本身应该进行编译,并将该代码排队*进行编译。 但这还不够,JVM必须具有在循环仍在运行时开始执行循环的已编译版本的能力-等待循环和封闭方法退出(这甚至可能不会发生)的效率很低。 因此,当用于循环的代码完成编译时,JVM将替换该代码(在堆栈上),并且该循环的下一次迭代将执行速度更快的编译版本。

This is OSR, that is, the code now is running in the most optimal way possible!

这就是OSR也就是说,代码现在正在以最佳方式运行!

*These queues are not strictly first in, first out; methods whose invocation counters are higher have priority. So even when a program starts execution and has lots of code to compile, this priority order helps ensure that the most important code will be compiled first. (This is another reason the compilation ID in the PrintCompilation output can appear out of order.)

*这些队列不是严格的先进先出; 调用计数器较高的方法具有优先级。 因此,即使程序开始执行并有很多代码要编译,该优先级顺序也有助于确保最重要的代码将首先被编译。 (这是PrintCompilation输出中的编译ID可能出现乱序的另一个原因。)

The next two attributes (s and !) are easy to understand. The “s means that the method is synchronized and the “!” means that the method has an exception handler as mentioned before.

接下来的两个属性( s )很容易理解。 “ s 表示该方法已同步,并且“ !” 表示该方法具有前面提到的异常处理程序。

The blocking flag (b) will never be printed by default in current versions of Java. It indicates that the compilation did not occur in the background.

默认情况下,在当前版本的Java中,永远不会打印阻塞标志( b )。 它指示编译未在后台进行。

The native attribute (n) indicates that the JVM generated a compiled code to facilitate the call into a native method.

本机属性( n )表示JVM生成了编译后的代码,以方便对本机方法的调用。

If tiered compilation has been disabled (with the option -XX:-TieredCompilation), the next field (tiered_level) will be blank. Otherwise, it will be a number indicating which tier has completed compilation. This number can go from 0 to 4 and the meaning is:

如果已禁用分层编译(使用-XX:-TieredCompilation选项),则下一个字段( tiered_level )将为空白。 否则,它将是一个数字,指示哪个层已完成编译。 该数字可以从0到4,含义是:

At tier 0, the code was not compiled, the code was just interpreted.

在第0层,未编译代码,仅对代码进行了解释。

At tiers 1, 2, 3 the code was compiled by C1 with different amounts of extra profiling. The most optimized of them are tier 1 since it has no profiling overhead.

在第1、2、3层,代码由C1用不同数量的额外配置文件进行编译。 其中最优化的是第1层,因为它没有配置开销。

At tier 4 the code is compiled by C2. It means that now the code was compiled at the highest possible level of compilation and was added in the code cache.

在第4层,代码由C2编译。 这意味着现在代码已以最高编译级别进行了编译,并已添加到代码缓存中。

Next comes the name of the method (method_name) being compiled which is printed like ClassName::method.

接下来是正在编译的方法的名称( method_name) ,它的打印方式类似于ClassName :: method

The next field is the size (in bytes) of the code being compiled. It is important to understand that this is the size of the Java bytecodes, not the size of the compiled code, so this value cannot be used to predict the code cache size.

下一个字段是正在编译的代码的大小 ( 以字节单位 )。 重要的是要了解这是Java字节码的大小,而不是已编译代码的大小,因此该值不能用于预测代码缓存的大小。

To finish, in some cases there is a message at the end of the compilation line that will indicate that some sort of deoptimization (deopt) has occurred, and they can be “made not entrant” or “made zombie”.

最后,在某些情况下,在编译行的末尾会出现一条消息,指示已发生某种形式的优化(deopt)(deopt) ,可以将它们“ 不让进入 ”或“ 使成为僵尸 ”。

If you saw C1, C2, and deoptimization, and didn’t understand, don’t worry, I’ll explain what they are below!

如果您看到了C1,C2和去优化,并且不了解,不用担心,我将在下面解释它们!

There are actually 2 compilers built into the JVM called C1 (also called Client Compiler) and C2 (also called Server Compiler).

JVM中实际上有2个编译器,分别称为C1(也称为Client Compiler)C2(也称为Server Compiler)

The C1 compiler is able to do the first three levels of compilation. Each is progressively more complex than the last one and the C2 compiler is responsible to do the fourth level.

C1编译器能够执行前三个编译级别 。 每一个都比上一个更加复杂,并且C2编译器负责执行第四个级别

The JVM decides what level of compilation to apply to a given block of code based on how often it is running and how complex or time-consuming it is. It is called “profiling” the code, so for any method which has a number of one to three, the code has been compiled using the C1 compiler and the higher the number is, the more “profiled” the code is.

JVM根据运行的频率以及运行的复杂程度和时间来决定将哪种编译级别应用于给定的代码块。 这就是所谓的“分析”代码 ,因此对于数量为1到3的任何方法,该代码都是使用C1编译器编译的,并且数字越大,代码越“经过分析”。

Just to be clear, “profiling” in Java is the process of monitoring various JVM levels parameters like Method Execution, Thread Execution, Object Creation, and Garbage Collection.

需要明确的是,Java中的“概要分析”是监视各种JVM级别参数的过程,例如方法执行,线程执行,对象创建和垃圾回收。

So, if the code has been called enough times, then we reach level 4 and the C2 compiler has been used instead.

因此,如果代码被调用了足够多次,那么我们将达到4级,而将使用C2编译器。

When it happens, means that our code is even more optimized than when it was compiled using the C1 compiler and the JVM can understand that this part of the code is being used so much that it will not only compile it to level 4, but it will also going to put that compiled code (level 4) into the code cache.

发生这种情况时,这意味着我们的代码比使用C1编译器进行编译时的代码更加优化,并且JVM可以理解,这部分代码已被广泛使用,不仅将其编译为4级,而且还将把已编译的代码(第4级)放入代码缓存中

Wait Julio, Code Cache?

等待Julio,代码缓存?

Actually it is really simple. Code cache is a special area of memory because that will be the quickest way for it to be accessible and therefore executed.

其实这很简单。 代码缓存是内存的特殊区域,因为这是对其进行访问和执行的最快方式。

Another common name for “level of the compilation” is “compilation tier” and how more it is, the more optimized the compiled code should be.

对“ 汇编级 ”另一种常见的名字是“ 编译层 ”,以及如何更多的是,更优化的编译的代码应该是。

So, you probably are wondering, why the JVM compilers don’t compile all code to level 4?

因此,您可能想知道,为什么JVM编译器没有将所有代码编译为4级?

And the answer is not so simple, but basically there is no free lunch when we are talking about resources, there is a tradeoff that we need to consider.

答案不是那么简单,但是当我们谈论资源时,基本上没有免费的午餐,需要权衡一下。

According to Mark Stoodley, JIT aggressively speculated on class hierarchy. In the Java ecosystem or in the Java language, calls are virtual by specification, which means they can be overridden by other classes. When you’re making a call to one of these methods, you don’t really know what the target is, it could be anything, it could be a class that hasn’t even been loaded yet.

根据Mark Stoodley的说法,JIT积极地猜测类的层次结构。 在Java生态系统或Java语言中,调用按规范是虚拟的,这意味着它们可以被其他类覆盖。 当您调用这些方法之一时,您实际上并不知道目标是什么,它可能是任何东西,也可能是尚未加载的类。

Note: JIT only can make this assumption because it is watching the Java Program, so it knows a lot about things like loaded classes, executed methods, and profiling.

注意:JIT只能做这个假设,因为它正在监视Java程序,因此它了解很多有关诸如装入的类,执行的方法和性能分析的信息。

Because of this dynamic nature of JVM, if it compiles a bytecode too early into native code, this can actually fool the compiler into speculating on something that doesn’t only have one target. It is, the compiler will generate a code pointing to the wrong object (target). Doing this, the compiler will generate a code that will be right for a while, but in cases that we have multiple targets (like multiple implementations of an Interface), this generated code will be wrong (pointing to the “wrong” implementation, for example).

由于JVM的这种动态特性,如果它过早地将字节码编译为本机代码,则实际上可能使编译器误以为仅基于一个目标的内容就被推测出来了。 就是说,编译器将生成指向错误对象(目标)的代码。 这样做,编译器将生成一会儿正确的代码,但是在我们有多个目标(例如接口的多个实现)的情况下,此生成的代码将是错误的(指向“错误”的实现,对于例)。

Knowing that the JIT has to generate backup paths, so it can deal with that situation when it happens. The problem is that we need to wait for the wrong code to be re-compiled to have a great performance again.

知道JIT必须生成备份路径,因此它可以在发生这种情况时对其进行处理。 问题是我们需要等待错误的代码重新编译才能再次具有出色的性能。

So, in other words, the JIT needs to re-optimize if the previous assumption was wrong, it is also called Deoptimization.

因此,换句话说,如果先前的假设错误,则JIT需要重新优化,这也称为Deoptimization

I’ll explain deoptimization better in the next part (3) of this series.

在本系列的下一部分(3)中,我将更好地解释反优化。

Hope you are enjoying this JVM journey!

希望您喜欢这个JVM旅程!

See you in the article!

在本文中见!

翻译自: https://medium.com/@julio.falbo/understand-jvm-and-jit-compiler-part-2-cc6f26fff721

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值