JVM之壓縮指針(CompressedOops)

對於32位機器,進程能使用的最大內存是4G。若是進程須要使用更多的內存,須要使用64位機器。html

對於Java進程,在oop只有32位時,只能引用4G內存。所以,若是須要使用更大的堆內存,須要部署64位JVM。這樣,oop爲64位,可引用的堆內存就更大了。java

注:oop(ordinary object pointer),即普通對象指針,是JVM中用於表明引用對象的句柄。git

在堆中,32位的對象引用佔4個字節,而64位的對象引用佔8個字節。也就是說,64位的對象引用大小是32位的2倍。github

64位JVM在支持更大堆的同時,因爲對象引用變大卻帶來了性能問題:c#

  1. 增長了GC開銷

64位對象引用須要佔用更多的堆空間,留給其餘數據的空間將會減小,從而加快了GC的發生,更頻繁的進行GC。緩存

  1. 下降CPU緩存命中率

64位對象引用增大了,CPU能緩存的oop將會更少,從而下降了CPU緩存的效率。bash

爲了可以保持32位的性能,oop必須保留32位。那麼,如何用32位oop來引用更大的堆內存呢?app

答案是壓縮指針(CompressedOops)。oop

JVM的實現方式是,再也不保存全部引用,而是每隔8個字節保存一個引用。例如,原來保存每一個引用0、一、2...,如今只保存0、八、16...。所以,指針壓縮後,並非全部引用都保存在堆中,而是以8個字節爲間隔保存引用。性能

在實現上,堆中的引用其實仍是按照0x0、0x一、0x2...進行存儲。只不過當引用被存入64位的寄存器時,JVM將其左移3位(至關於末尾添加3個0),例如0x0、0x一、0x2...分別被轉換爲0x0、0x八、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…

gist.github.com/arturmkrtch…

我的公衆號

更多文章,請關注公衆號:二進制之路

二進制之路
相關文章
相關標籤/搜索