Java integer ++我没有改变这个值

时间:2015-12-14 20:48:45

标签: java

我刚刚制作了这个简单的"程序":

$("#LineaDelTiempoDiv")

运行此程序后,我立即得到输出:

public static void main(String[] args) {
        int i = 1;
        int k = 0;
        while (true) {
            if(++i==0) System.out.println("loop: " + ++k);
        }
    }

好像(...) loop: 881452 loop: 881453 loop: 881454 loop: 881455 loop: 881456 loop: 881457 loop: 881458 (...) 总是0。

事实上,当我在Eclipse中调试时,在暂停程序时,i将始终为零。当单步执行循环时,i会递增,但在恢复和挂起调试器后,i再次为0。

当我将i更改为long时,在运行程序时,我需要等待一段时间才能看到第一个i。在调试器中,暂停程序后,loop: 1会增加:它不是0,所以它可以正常工作。

i作为int的问题是什么?

5 个答案:

答案 0 :(得分:49)

如果继续递增整数类型,它最终会溢出,成为一个较大的负值。如果你继续前进,它最终将再次变为0,并且循环将重复。

有一些方便的方法可以帮助避免意外溢出,例如Math.addExact(),但这些方法通常不会在循环中使用。

  

我知道它溢出来了。我很困惑,它快速溢出。我发现很奇怪,每次我暂停调试器时,我都是0。

暂停正在运行的线程时,请考虑线程缓慢调用遍历大量Java和本机操作系统代码的println()的可能性,而不是在您的测试中着陆的可能性。循环,它只是递增局部变量。您必须有一个非常快速的触发手指才能看到除print语句之外的任何内容。请尝试单步执行。

如果连续发生了40亿次,那么下次就会发生这种情况。在任何情况下,分支预测都会有所帮助,优化运行时可能会删除增量操作并完全测试,因为i的介入值永远不会被读取。

答案 1 :(得分:22)

作为JohannesD suggested in a comment,几乎不可能从0到Integer.MAX_VALUE(并且,在溢出之后,从-Integer.MAX_VALUE再次计数到0)这么快。

为了验证JIT在这里做了一些魔术优化的假设,我创建了一个稍微修改过的程序,引入了一些方法,可以更容易地识别部分代码:

class IntOverflowTest
{
    public static void main(String[] args) {
        runLoop();
    }

    public static void runLoop()
    {
        int i = 1;
        int k = 0;
        while (true) {
            if(++i==0) doPrint(++k);
        }
    }

    public static void doPrint(int k)
    {
        System.out.println("loop: " + k);
    }

}

使用javap -c IntOverflowTest发出并显示的字节码不会带来任何意外:

class IntOverflowTest {
  IntOverflowTest();
    Code:
       0: aload_0
       1: invokespecial #1                  
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #2                  
       3: return

  public static void runLoop();
    Code:
       0: iconst_1
       1: istore_0
       2: iconst_0
       3: istore_1
       4: iinc          0, 1
       7: iload_0
       8: ifne          4
      11: iinc          1, 1
      14: iload_1
      15: invokestatic  #3                  
      18: goto          4

  public static void doPrint(int);
    Code:
       0: getstatic     #4                  
       3: new           #5                  
       6: dup
       7: invokespecial #6                  
      10: ldc           #7                  
      12: invokevirtual #8                  
      15: iload_0
      16: invokevirtual #9                  
      19: invokevirtual #10                 
      22: invokevirtual #11                 
      25: return
}

它显然会增加局部变量(runLoop,偏移量4和11)。

但是,在Hotspot反汇编程序中运行带-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+PrintAssembly的代码时,机器代码最终会达到以下结果:

Decoding compiled method 0x00000000025c2c50:
Code:
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} {0x000000001bb40408} 'runLoop' '()V' in 'IntOverflowTest'
  #           [sp+0x20]  (sp of caller)
  0x00000000025c2da0: mov    %eax,-0x6000(%rsp)
  0x00000000025c2da7: push   %rbp
  0x00000000025c2da8: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - IntOverflowTest::runLoop@-1 (line 10)

  0x00000000025c2dac: mov    $0x1,%ebp          ;*iinc
                                                ; - IntOverflowTest::runLoop@11 (line 13)

  0x00000000025c2db1: mov    %ebp,%edx
  0x00000000025c2db3: callq  0x00000000024f6360  ; OopMap{off=24}
                                                ;*invokestatic doPrint
                                                ; - IntOverflowTest::runLoop@15 (line 13)
                                                ;   {static_call}
  0x00000000025c2db8: inc    %ebp               ;*iinc
                                                ; - IntOverflowTest::runLoop@11 (line 13)

  0x00000000025c2dba: jmp    0x00000000025c2db1  ;*invokestatic doPrint
                                                ; - IntOverflowTest::runLoop@15 (line 13)

  0x00000000025c2dbc: mov    %rax,%rdx
  0x00000000025c2dbf: add    $0x10,%rsp
  0x00000000025c2dc3: pop    %rbp
  0x00000000025c2dc4: jmpq   0x00000000025b0d20  ;   {runtime_call}
  0x00000000025c2dc9: hlt

可以清楚地看到它不再增加外部变量i。它只调用doPrint方法,增加单个变量(代码中的k),然后立即跳回到doPrint调用之前的点。

所以JIT确实似乎发现没有真正的"条件"涉及打印输出,并且代码相当于一个无限循环,只打印并递增单个变量。

这对我来说似乎是一个非常复杂的优化。我希望能够检测到这样的案例远非微不足道。但显然,他们设法做到了......

答案 2 :(得分:11)

你的循环溢出i。您没有break,因此在一段时间后,i回绕到0,这将打印语句并递增k。这也解释了为什么将int更改为long会导致打印速度变慢:long值溢出需要更长的时间。

答案 3 :(得分:10)

首先让我们看一下逻辑上的循环。

i会反复溢出。每循环2次 32 (约40亿次),输出将被打印,k将递增。

这是逻辑观点。但是,允许编译器和运行时进行优化,如果每秒钟后得到的值超过一个值,那么很明显必须进行这样的优化。即使使用现代分支预测,乱序执行等,我发现CPU不太可能在每个时钟周期内绕过一个紧密的循环(甚至我认为不太可能)。在调试器中你从未看到过零以外的事实强化了代码被优化的想法。

您提到使用“long”时需要更长的时间并且您确实看到了其他值。如果在未经优化的循环中使用“长”计数器,则可以预期值之间存在数十年。再次明显优化正在进行,但似乎优化者在完全优化掉无意义的迭代之前就放弃了。

答案 4 :(得分:4)

它并不总是0,它在循环结束后变为0(整数溢出),因此它首先变为Integer.MAX_VALUE,然后是Integer.MIN_VALUE,然后再次向上运行0。这就是为什么它似乎总是为0,但事实上它在变为0之前需要所有可能的整数值......一遍又一遍。