對於32位機器,程式能使用的最大記憶體是4G。如果程式需要使用更多的記憶體,需要使用64位機器。
對於Java程式,在oop只有32位時,只能引用4G記憶體。因此,如果需要使用更大的堆記憶體,需要部署64位JVM。這樣,oop為64位,可引用的堆記憶體就更大了。
注:oop(ordinary object pointer),即普通物件指標,是JVM中用於代表引用物件的控制程式碼。
在堆中,32位的物件引用佔4個位元組,而64位的物件引用佔8個位元組。也就是說,64位的物件引用大小是32位的2倍。
64位JVM在支援更大堆的同時,由於物件引用變大卻帶來了效能問題:
- 增加了GC開銷
64位物件引用需要佔用更多的堆空間,留給其他資料的空間將會減少,從而加快了GC的發生,更頻繁的進行GC。
- 降低CPU快取命中率
64位物件引用增大了,CPU能快取的oop將會更少,從而降低了CPU快取的效率。
為了能夠保持32位的效能,oop必須保留32位。那麼,如何用32位oop來引用更大的堆記憶體呢?
答案是壓縮指標(CompressedOops)。
JVM的實現方式是,不再儲存所有引用,而是每隔8個位元組儲存一個引用。例如,原來儲存每個引用0、1、2...,現在只儲存0、8、16...。因此,指標壓縮後,並不是所有引用都儲存在堆中,而是以8個位元組為間隔儲存引用。
在實現上,堆中的引用其實還是按照0x0、0x1、0x2...進行儲存。只不過當引用被存入64位的暫存器時,JVM將其左移3位(相當於末尾新增3個0),例如0x0、0x1、0x2...分別被轉換為0x0、0x8、0x10。而當從暫存器讀出時,JVM又可以右移3位,丟棄末尾的0。(oop在堆中是32位,在暫存器中是35位,2的35次方=32G。也就是說,使用32位,來達到35位oop所能引用的堆記憶體空間)
在JVM中(不管是32位還是64位),物件已經按8位元組邊界對齊了。對於大部分處理器,這種對齊方案都是最優的。所以,使用壓縮的oop並不會帶來什麼損失,反而提升了效能。
Oracle JDK從6 update 23開始在64位系統上會預設開啟壓縮指標。
32位HotSpot VM是不支援UseCompressedOops引數的,只有64位HotSpot VM才支援。
對於大小在4G和32G之間的堆,應該使用壓縮的oop。
檢視壓縮指標的工作模式
在VM啟動的時候,可以設定 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode 引數來確認壓縮指標的工作模式。
JDK 7
壓縮指標預設開啟:
$ java -server -Xms2G -Xmx2G -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode -version
heap address: 0x000000077ae00000, size: 2130 MB, zero based Compressed Oops
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
複製程式碼
JDK 8
壓縮指標預設開啟:
$ java -server -Xms2G -Xmx2G -XX:+UseConcMarkSweepGC -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode -version
heap address: 0x0000000080000000, size: 2048 MB, Compressed Oops mode: 32-bit
Narrow klass base: 0x0000000000000000, Narrow klass shift: 3
Compressed class space size: 1073741824 Address: 0x000000013fe20000 Req Addr: 0x0000000100000000
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
複製程式碼
關閉壓縮指標:
$ java -server -Xms2G -Xmx2G -XX:+UseConcMarkSweepGC -XX:-UseCompressedOops -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOopsMode -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
複製程式碼
例項比較
測試環境:JDK 1.8.0_121
測試程式碼
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
public class IntegerApplication {
public static void main(String[] args) {
List<Integer> intList = new LinkedList<>();
for (int i = 0; i < 2000000; i++) {
Integer number = new Integer(1);
intList.add(number);
}
Scanner scanner = new Scanner(System.in);
System.out.println("application is running...");
String tmp = scanner.nextLine();
System.exit(0);
}
}
複製程式碼
使用Eclipse Memory Analyzer檢視Integer物件數量與大小
先執行程式IntegerApplication,再通過mat檢視物件分配情況。
開啟壓縮指標
壓縮指標預設開啟(-XX:+UseCompressedOops)。
$ java IntegerApplication
application is running...
複製程式碼
每個Integer大小為:
64(Mark Word)+32(Compressed oops)+32(int)=128bits=16bytes
所有Integer總大小為:
2000256*16=32004096bytes
關閉壓縮指標
設定引數-XX:-UseCompressedOops,關閉壓縮指標。
$ java -XX:-UseCompressedOops IntegerApplication
application is running...
複製程式碼
每個Integer大小為:
64(Mark Word)+64(Compressed oops)+32(int)=160bits=20bytes
由於JVM記憶體分配需要根據字寬進行對齊,對於64位JVM,字寬為8個位元組。因此,一個Integer實際佔用24bytes,即192bits。
所有Integer總大小為:
2000256*24=48006144bytes
通過上面的例項可以看到,在開啟壓縮指標之後,oop大小確實是變成了32位,並且實際測試結果與理論分析是一致的。
Object Header
Object Header on a 64bit VM with compressed oops
Object Header on a 64bit VM without compressed oops
Object Header on a 32bit VM
參考
《Java效能權威指南》Scott Oaks
www.javacodegeeks.com/2016/05/com…
rednaxelafx.iteye.com/blog/101007…
個人公眾號
更多文章,請關注公眾號:二進位制之路