2020年1月1號全面小康社會已經來了,2020年了第一批90后也已經30歲了。在此元旦,新的一年托尼祝大家代碼永無bug,新的一年升職且加薪。
好了,言歸正傳,其實我們在編碼的過程中,有的時候真是不是特意寫bug的,我們也是想好好寫代碼的。
只不過有的時候不熟悉源碼,不小心就踩入坑中,而且摔個底朝天,程序員真的好難🤯。
序幕
上代碼if (a < b)
和if (a - b < 0)
?這不就是兩個參數的比較嗎?難道還有什么詭異之處?應該是一樣的呀。
你覺得上面代碼會打印什么?先別看下面的內容,第一感覺是什么樣??
字節碼
從字面理解就是比較a和b的大小,按道理執行速度、字面意思都一樣。其實機器是死的,它不會按照人類的理解去比較的。
有的時候我們需要通過字節碼去思考到底程序是怎么執行的?
// access flags 0x1
public test()V
@Lorg/junit/Test;()
L0
LINENUMBER 22 L0
LDC 2147483647
ISTORE 1
L1
LINENUMBER 23 L1
LDC -2147483648
ISTORE 2
L2
LINENUMBER 24 L2
ILOAD 1
ILOAD 2
IF_ICMPGE L3
L4
LINENUMBER 25 L4
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "a < b"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 27 L3
FRAME APPEND [I I]
ILOAD 1
ILOAD 2
ISUB
IFGE L5
L6
LINENUMBER 28 L6
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "a - b < 0"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L5
LINENUMBER 30 L5
FRAME SAME
RETURN
L7
托尼帶着大家讀字節碼。其實字節碼很簡單的,看上面截圖紅色框。挑選你熟悉的看LINENUMBER、LDC、ISTORE、IF_ICMPGE、ISUB
- LINENUMBER 代表的行號
- LDC 代表JVM 采用 LDC 指令將常量壓入棧中
- ISTORE 將一個數值從操作數棧存儲到局部變量表,還有``istore、istore_<n>、lstore、lstore_<n>等等。
- IF_ICMPGE 比較棧頂兩int型數值大小,當結果大於等於0時跳轉
- ISUB 將棧頂兩int型數值相減並將結果壓入棧頂
以上大概講了下JVM的指令,希望對你有點幫助。
再談溢出感知代碼
@Test
public void test() {
int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
System.out.println("a < b");
}
if (a - b < 0) {
System.out.println("a - b < 0");
}
}
這段代碼只能輸出 a-b<0 ,然后a<b 是不會打印出來的。
在JDK源碼中為什么ArrayList 中用if (a - b < 0)
而不用if (a < b)
?這個涉及到溢出感知代碼。
我們來分析List 如何擴容的grow 方法?
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
modCount++;
// Overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// Overflow-conscious code
// /把當前數組的長度賦給oldCapacity
int oldCapacity = elementData.length;
// 新的數組容量=老的數組長度的1.5倍,oldCapacity >> 1 相當於除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新的數組長度小於傳入的參數,那么當前新的
// 數組的長度則為傳入進來的長度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 新的數組的長度和 數組中最大值也就是(ArrayList 的數組最大值 Integer.MAX_VALUE - 8)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 傳入的舊的數組容量如果大於數組中最大默認值
// 則取最大的默認值
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
上面的注釋也加了,下面來說重點。 oldCapacity 這個值如果是非常接近Integer.MAX,那么執行這一句話
int newCapacity = oldCapacity + (oldCapacity >> 1);
,那么此刻newCapacity 是一個負數 ,如果JDK的代碼是這么判斷大小
if (newCapacity<minCapacity)
這種寫法,那么這個邏輯判斷條件永遠是TRUE,顯然和期望是相反的。
溢出
以下代碼是ArrayList 到數組擴容源碼, 我們來好好分析何為溢出感知代碼?英文叫做 overflow-conscious code。
在計算機中當整型變量的值為 Integer.MAX_VALUE(2147483647)時,繼續累加一個正的整型值,就會變成一個負數,這種情況稱之為上溢。
當整型變量的值為 Integer.MIN_VALUE(-2147483648) 時,繼續累加一個負的整型值,就會變成一個非負數,這種情況稱之為下溢。
好比水缸就能只能盛這么多水,你繼續添加水,水就會自然而然到溢出來了。
結論
在讀源碼過程中的我們要考慮到代碼的健壯性,邏輯到衍生性。希望這篇博文能讓你學到一點東西,哪怕一點點,我就覺得挺開心的。
衍生
在AarryList 的源碼中private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
,為什么要再減去 8 呢?
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
如上所訴,有些JVM保留頭部信息怕溢出了,所以設置了減8 。這也是JVM向上兼容的一種方式。
謝謝你們看到了這里,感謝🙏。新的一年祝升職加薪。
關注公眾號回復視頻、書籍關鍵字更多免費資料等着你。