java基礎教學專欄為大家介紹i 引發的bug。
大家好,身為日常寫bug修bug的我,今天帶給大家前幾天剛修復的一個意外。不得不承認,有我的地方總是會有這麼多bug。
故事的開始發生在前幾天,有一個不是很常用的匯出功能,被使用者回饋出,不管條件是怎麼樣,導出的數據只有一條,但是實際上根據條件查詢是有很多數據,而且頁面中也查詢出很多數據。 (這個問題已經被修復了,所以當時的Kibana日誌也找不到了)於是放下手上的工作,投入其中來看這個問題。
從問題的描述來分析,那麼只可能出現在以下情況:
題外話
寫到了這裡,突然想到了一個經典面試題,MQ訊息遺失的場景原因分析。哈哈哈,其實大致上也是這麼幾個角度分析。 (有機會寫MQ的文章)
題外話
#於是就一個個來分析:
因為這段程式碼都是寫在一整個方法裡面,導致Arthas排查起來就比較困難,只好一步步設定日誌進行排查。 (所以,如果是一大段邏輯,建議是拆分成duoge 子方法,一來在寫的時候思路明確,有一種模組化的概念,至於方法復用什麼的我就不多提了,基本操作;二是一但發生問題,排查起來也會方便點,經驗之談)。
最終定位到一個for迴圈裡面。
話不多說,我們直接來看程式碼。眾所周知,我向來是一個很保護公司程式碼的人,所以,我在這裡又不得不給大家模擬一下了。從問題的情況來看,是導出的物件記錄是空
import com.google.common.collect.Lists;import java.util.List;public class Test { public static void main(String[] args) { // 获取Customer数据,这里就简单模拟 List<Customer> customerList = Lists.newArrayList(new Customer("Java"), new Customer("Showyool"), new Customer("Soga")); int index = 0; String[][] exportData = new String[customerList.size()][2]; for (Customer customer : customerList) { exportData[index][0] = String.valueOf(index); exportData[index][1] = customer.getName(); index = index++; } System.out.println(JSON.toJSONString(exportData)); } }class Customer { public Customer(String name) { this.name = name; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }复制代码
這段程式碼看起來好像也沒什麼的,就是將Customer集合轉換成一個字串二維陣列。但是輸出結果顯示:這就符合我們說的,查詢出來有多條,但是輸出只有1條。
仔細觀察一下,我們可以發現,輸出的資料顯示都是最後一條,也就是說,Customer這個集合每次遍歷的時候,都是後者將前者進行覆蓋,也就是說,這個index的下標一直沒有變化過,一直是0。
這樣看來,我們的這個自增確實有點問題,那麼我們再簡單來寫一個模型
public class Test2 { public static void main(String[] args) { int index = 3; index = index++; System.out.println(index); } }复制代码
我們將上面的業務邏輯簡化成這樣一個模型,那麼這個結果毫無意外的是3。
那我們執行javap,看看JVM字節碼是如何解釋:
javap -c Test2 Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_1 7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_1 11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 14: return}复制代码
這裡我簡單講一下這裡的JVM字節碼指令(後面有機會再詳細寫寫文章)
首先我們需要先知道這裡存在兩個概念,操作數棧和局部變數表,這兩者是存在虛擬機器棧當中棧幀(stack frame)當中的一些資料結構,如圖:
我們可以簡單的理解為,操作數棧的作用是存放資料並且在堆疊中進行計算數據,而局部變數表則是存放變量的一些資訊。
然後我們來看看上面的指令:
0: iconst_3 (先將常數3壓入堆疊)
1: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
2: iload_1 (将第一个参数的值压入栈,也就是将3入栈,此时栈顶的值为3)
3: iinc 1, 1 (将第一个参数的值进行自增操作,那么此时index的值是4)
6: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
也就是说index这个参数的值是经历了index->3->4->3,所以这样一轮操作之后,index又回到了一开始赋值的值。
这样一来,我们发现,问题其实出在最后一步,在进行运算之后,又将原先栈中记录的值重新赋给变量,覆盖掉了 如果我们这样写:
public class Test2 { public static void main(String[] args) { int index = 3; index++; System.out.println(index); } } Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iinc 1, 1 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: iload_1 9: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 12: return}复制代码
可以发现,这里就没有最后一步的istore_1,那么在iinc之后,index的值就变成我们预想的4。
还有一种情况,我们来看看:
public class Test2 { public static void main(String[] args) { int index = 3; index = index + 2; System.out.println(index); } } Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iconst_2 4: iadd 5: istore_1 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 9: iload_1 10: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 13: return}复制代码
0: iconst_3 (先将常量3压入栈)
1: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
2: iload_1 (将第一个参数的值压入栈,也就是将3入栈,此时栈顶的值为3)
3: iconst_2 (将常量2压入栈, 此时栈顶的值为2,2在3之上)
4: iadd (将栈顶的两个数进行相加,并将结果压入栈。2+3=5,此时栈顶的值为5)
5: istore_1 (出栈操作,将值赋给第一个参数,也就是将5赋值给index)
看到这里各位观众老爷肯定会有这么一个疑惑,为什么这里的iadd加法操作之后,会影响栈里面的数据,而先前说的iinc不是在栈里面操作?好的吧,我们可以看看JVM虚拟机规范当中,它是这么描述的:
指令iinc对给定的局部变量做自增操作,这条指令是少数几个执行过程中完全不修改操作数栈的指令。它接收两个操作数: 第1个局部变量表的位置,第2个位累加数。比如常见的i++,就会产生这条指令
看到这里,我们知道,对于一般的加法操作之后复制没啥问题,但是使用i++之后,那么此时栈顶的数还是之前的旧值,如果此刻进行赋值就会回到原来的旧值,因为它并没有修改栈里面的数据。所以先前那个bug,只需要进行自增不赋值就可以修复了。
感謝各位能夠看到這裡,以上就是我處理這個bug的全部過程。雖然這只是一個小bug,但這一個小小的bug還是值得學習和思考的。今後還會繼續分享我所發現的bug以及知識點,如果我的文章對你有幫助,也希望各位大佬##按個讚 點
################# ##########,再次感謝大家的支持! ###############相關免費學習推薦:#########java基礎教學############
以上是解決一次i++引發的bug的詳細內容。更多資訊請關注PHP中文網其他相關文章!