对于32位机器,进程能使用的最大内存是4G。如果进程需要使用更多的内存,需要使用64位机器。
对于Java进程,在oop只有32位时,只能引用4G内存。因此,如果需要使用更大的堆内存,需要部署64位JVM。这样,oop为64位,可引用的堆内存就更大了。
注:oop(ordinary object pointer),即普通对象指针,是JVM中用于代表引用对象的句柄。
在堆中,32位的对象引用占4个字节,而64位的对象引用占8个字节。也就是说,64位的对象引用大小是32位的2倍。
64位对象引用需要占用更多的堆空间,留给其他数据的空间将会减少,从而加快了GC的发生,更频繁的进行GC。
64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。
为了能够保持32位的性能,oop必须保留32位。那么,如何用32位oop来引用更大的堆内存呢?
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 参数来确认压缩指针的工作模式。
压缩指针默认开启:
$ 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) 复制代码
压缩指针默认开启:
$ 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); } } 复制代码
先运行程序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位,并且实际测试结果与理论分析是一致的。