关于java:解释器如何解释代码?

How does an interpreter interpret the code?

为了简单起见,想象一下这个场景,我们有一台2位计算机,它有一对2位寄存器,名为r1和r2,并且只适用于立即寻址。

假设位序列00意味着添加到我们的CPU。01表示将数据移动到R1,10表示将数据移动到R2。

所以这台计算机有一种汇编语言和一个汇编程序,在这里编写一个示例代码

1
2
3
mov r1,1
mov r2,2
add r1,r2

简单地说,当我将此代码组装为本机语言时,文件将类似于:

1
0101 1010 0001

上面的12位是以下内容的本机代码:

1
Put decimal 1 to R1, Put decimal 2 to R2, Add the data and store in R1.

所以这基本上就是编译代码的工作原理,对吗?

假设有人为这个体系结构实现了一个JVM。在Java中,我将编写代码如下:

1
int x = 1 + 2;

JVM将如何解释这段代码?我的意思是最终相同的位模式必须传递给CPU,不是吗?所有的CPU都有许多可以理解和执行的指令,它们毕竟只是一些位。假设编译后的Java字节码看起来像这样:

1
1111 1100 1001

或者别的……这是否意味着在执行时,解释会将此代码更改为0101 1010 0001?如果是的话,它已经在本地代码中了,那么为什么它说JIT只在几次之后才开始工作呢?如果它不能准确地将其转换为0101 1010 0001,那么它会做什么?它如何使CPU做加法?

也许我的假设有错误。

我知道解释很慢,编译后的代码很快但不可移植,而且虚拟机"解释"了一个代码,但怎么解释呢?我在寻找"如何准确/技术性地解释"的方法。欢迎使用任何指针(如书籍或网页)代替答案。


不幸的是你的CPU架构的描述要清晰这真的太度与所有的中间步骤。好的,我想要写的伪伪x86汇编语言的C和A的方式,这是我们有了清晰的不熟悉C或x86。

在JVM字节码编译的东西可能看起来像这样:

1
2
3
4
ldc 0 # push first first constant (== 1)
ldc 1 # push the second constant (== 2)
iadd # pop two integers and push their sum
istore_0 # pop result and store in local variable

安切洛蒂的解释器(a二进制编码)在阵列进行说明,和Freundlich指数指电流指令。它也有阵列的常数,可作为堆栈区和一个内存和一个本地变量。的话,这个解释器环看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (true) {
    switch(instructions[pc]) {
    case LDC:
        sp += 1; // make space for constant
        stack[sp] = constants[instructions[pc+1]];
        pc += 2; // two-byte instruction
    case IADD:
        stack[sp-1] += stack[sp]; // add to first operand
        sp -= 1; // pop other operand
        pc += 1; // one-byte instruction
    case ISTORE_0:
        locals[0] = stack[sp];
        sp -= 1; // pop
        pc += 1; // one-byte instruction
    // ... other cases ...
    }
}

这是一个C代码编译为机器代码和运行。你可以看到,它是高度动态:它inspects每个字节码指令的指令执行,这是每一次,和所有的值是通过堆栈(IU的RAM)。

它可能发生在实际加在A寄存器,是不是很不同的代码添加到从什么是Java编译器将代码或机器。这是一个在C编译器可能是摘录从上面到体操(伪x86):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.ldc:
incl %esi # increment the variable pc, first half of pc += 2;
movb %ecx, program(%esi) # load byte after instruction
movl %eax, constants(,%ebx,4) # load constant from pool
incl %edi # increment sp
movl %eax, stack(,%edi,4) # write constant onto stack
incl %esi # other half of pc += 2
jmp .EndOfSwitch

.addi
movl %eax, stack(,%edi,4) # load first operand
decl %edi # sp -= 1;
addl stack(,%edi,4), %eax # add
incl %esi # pc += 1;
jmp .EndOfSwitch

你可以看到的是,operands for the相加而来的记忆是hardcoded的用途,即使他们的Java程序的恒定。这是因为他们的解释器,是不恒定的。这是一次然后必须编译解释器可以执行所有的大学课程:专业,没有生成的代码。

的目的是在JIT编译器生成的代码是:做专业。a JIT方式CAN分析堆栈是用来传输各种数据,实际值和常数在程序序列,进行计算,生成代码,并获得更多相同的东西。在我们的例子程序,是分配到一个当地的变量替换为寄存器,访问一个表的运动常数的常数寄存器(movl %eax, $1),和重定向到正确的机器accesses堆栈寄存器。忽略一些更多的基准(复制传播常数的折叠和死代码消除,这通常会做的),它可能是这样的:最终的代码

1
2
3
4
5
movl %ebx, $1 # ldc 0
movl %ecx, $2 # ldc 1
movl %eax, %ebx # (1/2) addi
addl %eax, %ecx # (2/2) addi
# no istore_0, local variable 0 == %eax, so we're done


并非所有计算机都有相同的指令集。Java字节码是一种世界语——一种人工语言来改善交流。Java VM将通用Java字节码转换为它运行的计算机的指令集。

那么,jit在这里是如何计算的呢?JIT编译器的主要目的是优化。将某个字节码转换成目标机器码的方法通常是不同的。最理想的翻译性能通常不明显,因为它可能依赖于数据。程序在不执行算法的情况下分析算法的程度也有限制——暂停问题是一个众所周知的限制,但不是唯一的限制。因此,JIT编译器所做的是尝试不同的可能的翻译,并测量它们使用程序处理的实际数据执行的速度。所以需要执行很多次,直到JIT编译器找到完美的翻译。


Java中的一个重要步骤是编译器首先将EDCOX1代码0代码转换成EDCOX1×1文件,其中包含Java字节码。这是很有用的,因为您可以获取.class文件并在任何了解此中间语言的机器上运行它们,然后逐行或逐块地在现场进行翻译。这是Java编译器+解释器最重要的功能之一。您可以直接编译Java源代码到本机二进制,但这否定了编写原始代码一次并能够在任何地方运行它的想法。这是因为编译后的本机二进制代码将只在编译时所使用的硬件/操作系统体系结构上运行。如果您想在另一个体系结构上运行它,您必须在那个体系结构上重新编译源代码。编译到中间级别的字节码后,不需要拖拽源代码,只需要拖拽字节码。这是另一个问题,因为您现在需要一个能够解释和运行字节码的JVM。因此,编译到中间层字节码(解释器随后运行)是过程的一个组成部分。

至于代码的实际实时运行:是的,JVM最终将解释/运行一些可能与本机编译的代码相同或不相同的二进制代码。在一个单行的例子中,它们看起来可能表面上是一样的。但是这个解释通常并不预编译所有的东西,而是通过字节码并逐行或逐块翻译成二进制。这其中有优点和缺点(与本机编译的代码相比,如C和C编译器),还有许多在线资源可供进一步阅读。看看我的答案,或者这个,或者这个。


解释器是一个无限循环的简化,在一个大的开关。它读取的Java字节码(或一些内部表示)和模拟(CPU执行它。这样的真正的CPU executes代码的解释器,它模拟的虚拟处理器。这是最好的慢。单指令添加虚拟函数调用需要的三个数的和许多其他业务。单虚拟指令需要几房到执行指令。这是一个无记忆。所以你有两个真正的和高效的仿真指令指针寄存器堆栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while(true) {
    Operation op = methodByteCode.get(instructionPointer);
    switch(op) {
        case ADD:
            stack.pushInt(stack.popInt() + stack.popInt())
            instructionPointer++;
            break;
        case STORE:
            memory.set(stack.popInt(), stack.popInt())
            instructionPointer++;
            break;
        ...

    }
}

当有多个时代的理解方法,JIT编译器踢。它会读取所有的指令和一个或更多的虚拟生成的本地指令是不一样的。在这里我的文本字符串,它会产生额外的组装需要组装到本地的二进制转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(Operation op : methodByteCode) {
    switch(op) {
        case ADD:
            compiledCode +="popi r1"
            compiledCode +="popi r2"
            compiledCode +="addi r1, r2, r3"
            compiledCode +="pushi r3"
            break;
        case STORE:
            compiledCode +="popi r1"
            compiledCode +="storei r1"
            break;
        ...

    }
}

后生成的本地代码的JVM是一个地方,希望它复制,标记为可执行和指导本地区的解释器来解释它的"invoke方法的字节码,这是invoked酒店。单虚拟指令可能需要超过一个安静的原生指令但这将接近几乎是在编译到本地代码时(如在C或C + +)。编辑是将通常比多做解释,但安切洛蒂的选择方法和只读只读一次的。