一个由JIT优化引发的问题

本文探讨了一段导致死循环的Java代码,并通过分析JIT编译后的汇编代码揭示了问题根源。介绍了如何使用volatile关键字避免此类问题,并深入讨论了其背后的原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

最近遇到了一段代码,仔细分析了下异常的原因,过程中遇到了很多问题,在这里总结下

public class NoVisiabilityTest {

	private static class ReadThread extends Thread {

		private boolean ready;

		private int number;

		public void run() {
			while (!ready) {
				number++;
				//System.out.println("OK");
			}
			System.out.println(ready);
		}

		public void readyOn() {
			this.ready = true;
		}
	}

	public static void main(String[] args) throws InterruptedException {
		ReadThread readThread = new ReadThread();
		readThread.start();
		Thread.sleep(200);
		readThread.readyOn();
		System.out.println(readThread.ready);
	}
}

代码的出处:http://cache.baiducontent.com/c?m=9d78d513d99f1ceb03accf2d1a17a736420adb2577c0d1653983d20e87231b1f483ca5fd65351177ced8273356eb1e4bea87672f681e78e4df9b9f4aabe8c37f38885133671cf0410f&p=8162c54ad5c042f50be296234e55cd&newp=c379d25483904ead08e2977d0e0083231615d70e3cd0da1f&user=baidu&fm=sc&query=NoVisiabilitytest&qid=9e9aba67000027e7&p1=1

首先这是一段有问题的死循环代码,由于自己对于文中的解释不是特别理解,因此自己实践了下

问题解决

解决死循环的问题可以通过valotile命名ready变量,或者

while (!ready) {
				number++;
				System.out.println("OK");
			}

相关原理你可以参考上述出处,或者继续跟下去。每个人的观点都不一样,我也不确保自己的见解一定是正确的

问题跟踪

简化代码

public class Thread1{

                private boolean ready;

                private int number;

                public void run0() {
                        while (!ready) {
                                number++;
                                //System.out.println("OK");
                        }
                        System.out.println(ready);
                }

                public void readyOn() {
                        this.ready = true;
                }
                public static void main(String[] args) {
                        Thread1 inst = new Thread1();
                        //System.out.println("get ready");
                        inst.run0();
                }
        }

编译期

javap -c Thread1

Compiled from "Thread1.java"
public class Thread1 {
  public Thread1();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public void run0();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field ready:Z
       4: ifne          20
       7: aload_0       
       8: dup           
       9: getfield      #3                  // Field number:I
      12: iconst_1      
      13: iadd          
      14: putfield      #3                  // Field number:I
      17: goto          0
      20: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_0       
      24: getfield      #2                  // Field ready:Z
      27: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
      30: return        

  public void readyOn();
    Code:
       0: aload_0       
       1: iconst_1      
       2: putfield      #2                  // Field ready:Z
       5: return        

  public static void main(java.lang.String[]);
    Code:
       0: new           #6                  // class Thread1
       3: dup           
       4: invokespecial #7                  // Method "<init>":()V
       7: astore_1      
       8: aload_1       
       9: invokevirtual #8                  // Method run0:()V
      12: return        
}

运行时

运行时优化比较难以定位,需要安装hsdis插件,环境搭建可以参照:http://www.infoq.com/cn/articles/zzm-java-hsdis-jvm/

CMD:

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Thread1.run0 -XX:CompileCommand=compileonly,*Thread1.run0 Thread1 > T1.log

ASM:

Decoding compiled method 0x0000000110ac10d0:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Constants]
  # {method} 'run0' '()V' in 'Thread1'
  #           [sp+0x20]  (sp of caller)
  0x0000000110ac1200: mov    0x8(%rsi),%r10d
  0x0000000110ac1204: shl    $0x3,%r10
  0x0000000110ac1208: cmp    %r10,%rax
  0x0000000110ac120b: jne    0x0000000110a96960  ;   {runtime_call}
  0x0000000110ac1211: data32 xchg %ax,%ax
  0x0000000110ac1214: nopl   0x0(%rax,%rax,1)
  0x0000000110ac121c: data32 data32 xchg %ax,%ax
[Verified Entry Point]
  0x0000000110ac1220: mov    %eax,-0x14000(%rsp)
  0x0000000110ac1227: push   %rbp
  0x0000000110ac1228: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - Thread2::run0@-1 (line 8)
  0x0000000110ac122c: mov    %rsi,%r10
  0x0000000110ac122f: movzbl 0x10(%rsi),%r11d
  0x0000000110ac1234: test   %r11d,%r11d
  0x0000000110ac1237: jne    0x0000000110ac124c  ;*ifne
                                                ; - Thread2::run0@4 (line 8)
  0x0000000110ac1239: mov    0xc(%rsi),%r8d     ;*getfield number
                                                ; - Thread2::run0@9 (line 9)
  0x0000000110ac123d: inc    %r8d               ;*iadd
                                                ; - Thread2::run0@13 (line 9)
  0x0000000110ac1240: mov    %r8d,0xc(%r10)     ; OopMap{r10=Oop off=68}
                                                ;*goto
                                                ; - Thread2::run0@17 (line 9)
  0x0000000110ac1244: test   %eax,-0x133124a(%rip)        # 0x000000010f790000
                                                ;*goto
                                                ; - Thread2::run0@17 (line 9)
                                                ;   {poll}
  0x0000000110ac124a: jmp    0x0000000110ac123d
  0x0000000110ac124c: mov    $0x1c,%esi
  0x0000000110ac1251: mov    %r10,%rbp
  0x0000000110ac1254: data32 xchg %ax,%ax
  0x0000000110ac1257: callq  0x0000000110a97f20  ; OopMap{rbp=Oop off=92}
                                                ;*getstatic out
                                                ; - Thread2::run0@20 (line 11)
                                                ;   {runtime_call}
  0x0000000110ac125c: callq  0x000000011041bb72  ;*getfield number
                                                ; - Thread2::run0@9 (line 9)
                                                ;   {runtime_call}
  0x0000000110ac1261: callq  0x000000011041bb72  ;   {runtime_call}

从0x0000000110ac123d到0x0000000110ac124a已经可以看出陷入了死循环(0x0000000110ac1244行轮询safepoint),而翻译过来的java代码类似于:

while (!ready) {
    if(ready) {
        System.out.println("OK");
    } else {
        while(true) {
            number++;
        }
    }
}

一个uncommon_trap()引发的思考

代码如下:

public class Thread2{

                private boolean ready;

                private int number;

                public void run0() {
                        while (!ready) {
                                number++;
                                System.out.println("OK");
                        }
                        System.out.println(ready);
                }

                public void readyOn() {
                        this.ready = true;
                }
                public static void main(String[] args) {
                        Thread1 inst = new Thread1();
                        //System.out.println("get ready");
                        inst.run0();
                }
        }

JIT反编译后的汇编如下:

[Verified Entry Point]  
  0x00000001104c1220: mov    %eax,-0x14000(%rsp)  
  0x00000001104c1227: push   %rbp  
  0x00000001104c1228: sub    $0x10,%rsp         ;*synchronization entry  
                                                ; - Thread1::run0@-1 (line 8)  
  0x00000001104c122c: movzbl 0x10(%rsi),%r10d  
  0x00000001104c1231: test   %r10d,%r10d  
  0x00000001104c1234: je     0x00000001104c1249  ;*ifne  
                                                ; - Thread1::run0@4 (line 8)  
  0x00000001104c1236: mov    %rsi,%rbp  
  0x00000001104c1239: mov    $0x1e,%esi  
  0x00000001104c123e: nop  
  0x00000001104c123f: callq  0x0000000110497f20  ; OopMap{rbp=Oop off=68}  
                                                ;*getstatic out  
                                                ; - Thread1::run0@28 (line 12)  
                                                ;   {runtime_call}  
  0x00000001104c1244: callq  0x000000010fe1bb72  ;*getstatic out  
                                                ; - Thread1::run0@28 (line 12)  
                                                ;   {runtime_call}  
  0x00000001104c1249: incl   0xc(%rsi)          ;*synchronization entry  
                                                ; - Thread1::run0@-1 (line 8)  
  0x00000001104c124c: mov    %rsi,%rbp  
  0x00000001104c124f: mov    $0x1e,%esi  
  0x00000001104c1254: data32 xchg %ax,%ax  
  0x00000001104c1257: callq  0x0000000110497f20  ; OopMap{rbp=Oop off=92}  
                                                ;*getstatic out  
                                                ; - Thread1::run0@17 (line 10)  
                                                ;   {runtime_call}  
  0x00000001104c125c: callq  0x000000010fe1bb72  ;*getstatic out  
                                                ; - Thread1::run0@17 (line 10)  
                                                ;   {runtime_call} 
......
 OK
 OK
 OK
......
 Decoding compiled method 0x00000001104bf3d0:  
Code:  
[Entry Point]  
[Verified Entry Point]  
[Constants]  
  # {method} 'run0' '()V' in 'Thread1'  
  0x00000001104bf540: callq  0x000000010fe1bb72  ;   {runtime_call}  
  0x00000001104bf545: data32 data32 nopw 0x0(%rax,%rax,1)  
  0x00000001104bf550: mov    %eax,-0x14000(%rsp)  
  0x00000001104bf557: push   %rbp  
  0x00000001104bf558: sub    $0x10,%rsp  
  0x00000001104bf55c: mov    (%rsi),%rbp  
  0x00000001104bf55f: mov    %rsi,%rdi  
  0x00000001104bf562: movabs $0x10fe75a70,%r10  
  0x00000001104bf56c: callq  *%r10  
  0x00000001104bf56f: mov    0x8(%rbp),%r11d    ; implicit exception: dispatches to 0x00000001104bf601  
  0x00000001104bf573: cmp    $0xef610642,%r11d  ;   {oop('Thread1')}  
  0x00000001104bf57a: jne    0x00000001104bf5dc  
  0x00000001104bf57c: jmp    0x00000001104bf594  
  0x00000001104bf57e: xchg   %ax,%ax            ;*aload_0  
                                                ; - Thread1::run0@0 (line 8)  
  0x00000001104bf580: lea    (%r12,%r10,8),%rsi  ;*getstatic out  
                                                ; - Thread1::run0@17 (line 10)  
  0x00000001104bf584: movabs $0x7d5654668,%rdx  ;   {oop("OK")}  
  0x00000001104bf58e: nop  
..................

上面的Hotspot log可以看到有两个问题:

1、该方法被执行12000+次会被二次编译了

2、第一次编译后的代码竟然没有jmp,看不出循环怎么实现的

咨询了下撒加,得到原因如下:

我用了-Xcomp,在第一次调用Thread2.run0()之前PrintStream类还没有加载。所以第一次编译得到的就是在while循环内和循环后各有一个uncommon_trap()的调用。于是就没有循环了,无论循环与否都跳回解释器去跑。跑到足够热了就触发了第二次编译。

可以参考博客:http://rednaxelafx.iteye.com/blog/1038324

Q&A

1、JVM不是解释执行的么,怎么会扯到汇编代码的?

方法被执行的次数+方法体内总循环的执行次数 > 阈值,会触发JIT编译成本地代码,你试着把Thread.sleep(200)去掉也就没有死循环了,但是随着时间的推移,势必又会出现死循环的情况

2、valotile命名变量可以解决这一问题,而一般并发程序中都是如此设计的,原理是什么?真的能彻底解决么?

其实从汇编代码可以推敲出valotile的原理,你可以写段代码试着JIT编译后参考下。

我更加赞同的观点是:volatile关键字防止了编绎器优化,所以对于变量不会被放到寄存器里,或者被优化掉。但是volatile并不能防止CPU从Cache中读取数据,可以参考博客:http://blog.youkuaiyun.com/hengyunabc/article/details/26822801

转载于:https://my.oschina.net/tryUcatchUfinallyU/blog/357480

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值