阅读 396

JVM之压缩指针(CompressedOops)

对于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在支持更大堆的同时,由于对象引用变大却带来了性能问题:

  1. 增加了GC开销

64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。

  1. 降低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…

gist.github.com/arturmkrtch…

个人公众号

更多文章,请关注公众号:二进制之路

二进制之路

关注下面的标签,发现更多相似文章
评论