Java中包裝類與基本類型運算的性能比較(Integer i += 1)


本文主要從字節碼內存占用的角度介紹自動拆箱裝箱對運算中性能的影響。

如果要看懂字節碼,需要了解JVM的虛擬機棧的結構和代碼的執行流程,可參閱《深入理解Java虛擬機》

本文部分參考了如下文章的內容:

Java 性能要點:自動裝箱/ 拆箱 (Autoboxing / Unboxing)

JAVA中包裝類的作用

深入淺出 Java 中的包裝類

深入剖析Java中的裝箱和拆箱(淺度和深度都有了)

最近在做華為2020年的軟件挑戰賽,其中經常會用List來保存Integer類的數值,在做性能優化的時候想到了裝箱/拆箱的性能損失,特意實驗了一下。

以下代碼從0開始一直加到int類型所能表達的最大值。

long start = System.currentTimeMillis();
Long sum = 0L; // 使用包裝類相加
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗時:"+(end-start)/1000.0);
// 輸出:
// 2305843005992468481
// 耗時:15.175
start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i; // 使用基本數據類型相加
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗時:"+(end-start)/1000.0);
// 輸出:
// 2305843005992468481
// 耗時:1.643

兩者代碼的區別僅僅在於前者的sum為包裝類Long,后者的sum為基本類型long

原因分析

簡而言之:裝箱和拆箱造成的性能損失。裝箱會在堆空間中創建包裝類,頻繁的創建會導致導致堆空間碎片很多

包裝類(Wrapper Class): Java是一個面向對象的編程語言,但是Java中的八種基本數據類型卻是不面向對象的,為了使用方便和解決這個不足,在設計類時為每個基本數據類型設計了一個對應的類進行代表,這樣八種基本數據類型對應的類統稱為包裝類(Wrapper Class),包裝類均位於java.lang包。

裝箱(boxing):基本數據類型->包裝類型(以Integer為例)

int a = 5;
// 構造函數法
Integer a1 = new Integer(a); // 數值類型轉包裝類
Integer a2 = new Integer(5);
Integer a3 = new Integer("5"); // 字符串類型轉包裝類
// Integer.valueOf()
Integer a4 = Integer.valueOf(a);
Integer a5 = Integer.valueOf(5);

此處需要注意的是,包裝類的初始化會在堆中申請空間。不管使用new Integer()還是Integer.valueOf()。因為Integer.valueOf()本質上是通過工具類的形式,創建了新的Integer對象,不過是要先查詢創建的數值是否在緩存池(-128, 127)中。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
    return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

拆箱(unboxing):包裝類->基本數據類型(以Integer為例)

Integer b = new Integer(5); // 假設我們已經有了一個包裝類
// 調用xxValue()方法
int b1 = b.intValue(); // 轉化為int類型
double b2 = b.doubleValue(); // 轉化為double類型
long b3 = b.longValue(); // 轉化為long類型

JDK自從1.5版本以后,就引入了自動拆裝箱的語法,也就是在進行基本數據類型和對應的包裝類轉換時,系統將自動進行

//編譯器執行了Integer a  = Integer.valueOf(5)
Integer a = 5;
//自動拆箱,實際上執行了 int b = a.intValue()
int b = a;

字節碼角度理解包裝類+=發生了什么?

以一段簡單的代碼為例:

Integer a = 5;
a+=1;

其字節碼內容為(字節碼的行號不一定連續):

 0 iconst_5
 1 invokestatic #16 <java/lang/Integer.valueOf>
 4 astore_1
 5 aload_1
 6 invokevirtual #22 <java/lang/Integer.intValue>
 9 iconst_1
10 iadd
11 invokestatic #16 <java/lang/Integer.valueOf>
14 astore_1

我們可以看出:

Integer a = 5對應字節碼的0,1,4:把常量5壓入棧中->隱式調用了Integer,valueOf()->存儲新的包裝類的地址值到變量a

a+=1對應了6-14行:加載變量a -> 拆箱為int基本類型(調用intValue)-> 把常量1壓入棧中 -> 彈出1和拆箱后的aint類型的值並相加,將相加后的值壓回到棧中(還是int)->調用Integer.valueOf(),將結果裝箱-> 存儲新的包裝類的地址值到變量a

小結:我們可以看出對包裝類進行運算,則需要先拆箱,后裝箱。這一過程還需要向堆中申請空間。相比而言,基本類型的運算則高效很多

int a = 5;
a += 1;

其字節碼為:

 0 iconst_5
 1 istore_1
 2 iinc 1 by 1

基本數據類型的運算,都是在棧中進行。

從內存占用的角度理解包裝類的+=

使用JDK自帶的Jconsole工具,來查看前面兩段代碼的內存占用。
Long(包裝類)的內存占用情況
包裝類運算的內存占用

基本數據類型的內存占用情況
基本數據類型的內存占用

程序剛開始執行時,僅占用8M左右的內存。使用包裝類存儲運算結果,會導致內存占用高達80M,峰值達到138M,並且年輕代進行了505次垃圾回收;使用基本數據類型時,內存占用始終保持在8M左右,並且沒有垃圾回收

因此,我們可以得出:對包裝類進行頻繁的運算會占用很多內存空間,導致執行效率不高。

增強型foreach循環遍歷包裝類的集合

我們知道了包裝類進行運算會使得效率低下,那么在遍歷的時候,我們要怎么做呢?哪種寫法會好些呢?

例如下面這幾種相加的方法有何區別呢?

// 使用流生成0到40000000的List
List<Long> arr = LongStream.rangeClosed(0, 40000000).boxed().collect(Collectors.toList());
// ①在for中取取變量時為基本類型, 結果存放為基本類型
long start = System.currentTimeMillis();
long sum = 0;
for(long l:arr) { // 臨時變量l在棧中創建,直接將arr中的元素轉化為LongValue
	sum += l; 
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗時:"+(end-start)/1000.0);
// ② 在for中取取變量時為包裝類型,結果存放為基本類型
start = System.currentTimeMillis();
long Sum = 0L;
for(Long l:arr) { // 臨時變量l在堆中創建
	Sum += l; // 相加時轉時調用l.longValue()
}
System.out.println(Sum);
end = System.currentTimeMillis();
System.out.println("耗時:"+(end-start)/1000.0);
// ③ 在for中取取變量時為包裝類型,結果存放為包裝類
start = System.currentTimeMillis();
Long SUM = 0L;
for(Long l:arr) {// 臨時變量l在堆中創建
	SUM += l; // l和SUM都調用longValue(),完成相加后再裝箱
}
System.out.println(SUM);
end = System.currentTimeMillis();
System.out.println("耗時:"+(end-start)/1000.0);
// 輸出:
//800000020000000
//①耗時:0.269
//800000020000000
//②耗時:0.326
//800000020000000
//③耗時:1.089

字節碼文件此處就不貼了,帶上循環顯得有些繁瑣。變量重復使用還需要局部變量表。

第一種方式的速度最快,其局部變量l基本類型,僅需要在棧中申請空間;其結果Sum也保存在中;拆箱發生在元素取出時

第二種方式比第一種略慢,其局部變量l包裝類,需要在堆中申請空間;但其結果也保存在中;拆箱發生在兩數相加時

第三種方式最慢,其局部變量l包裝類,需要在堆中申請空間;其結果SUM也需要在中申請空間;要命的是,兩數相加時均發生拆箱操作,相加之后又要創建新的包裝類

總結

  • 包裝類在進行計算時(包裝類與包裝類/包裝類與基本類型)都會自動拆箱

  • 其結果如果仍然用包裝類存儲,則會再次發生裝箱

  • 頻繁的拆箱裝箱會導致內存碎片過多,引發頻繁的垃圾回收影響性能


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM