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在支持更大堆的同时,由于对象引用变大却带来了性能问题:
- 增加了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…
gist.github.com/arturmkrtch…
以上是 JVM之压缩指针(CompressedOops) 的全部内容, 来源链接: utcz.com/z/512792.html