问题背景

之前看到一篇文章说到es的内存调优, 文章说到现在的服务器越来越大, 那么给JVM设置多大的内存比较合适呢?太大的话虽然内存充足,但是会带来gc停顿时间的增大,对于es这一类需要快速响应的应用来说是不可接受的,太小则会有OOM的风险。因此内存需要设置的刚刚好。

What is CompressedOops

压缩指针(compressed oop, or compressed ordinary object pointer)则提供了一种既可以享受到大内存的好处,同时可以节约内存空间并且提高性能的方法。在某些情况下,它可以达到性能与停顿时间的平衡。

ref On an LP64 system, the heap used by a given program might have to be around 1.5 times larger than when it is run on an ILP32 system.

在64位系统中,对象指针也是64位,因此可以使用更大的内存,而在32位系统中是32位,最多只能使用4G的内存。对象指针长度的增大也会带来内存开销,而这种开销的差距在64位和32位系统之间可能是1.5倍,也就是说在如果32位系统使用4G内存达到的效果可能64位系统使用6G内存才能达到。
在很多情况下,这种开销是不必要的,并且会提高系统停顿时间,因为很多时候应用只需要32位指针就够了。
压缩指针提供了一种折中的选择,在JDK7中(64位),如果堆小于4G,JVM会默认使用32位对象指针。
那显然这种优化只能覆盖一部分的情况,为了享受大内存,很多应用会开比4G大得多的堆,这时候是不是就必须用64位指针了呢? 答案是不是,在堆小于32G(不精确)的情况下,此时对象地址只需要使用35位地址(32位=4G, 4G * 23 = 32G)即可, 此时如果操作系统允许,JVM会将对象指针右移3位进行存储,也就是说还是只需要32位地址就行了,此时会有两个问题:

  • 那么这时候会出现的问题就是右边3位丢失了怎么办?
  • 存储的地址和实际的地址相差八倍,即每两个指针之间有8个字节的空档,这其中存在内存的‘空隙’如何处理?

第一个问题最简单的情况下就是右边3位全是0,那么在转换过程中只需要右移三位(压缩过程)和左移三位(解压缩过程)即可(也即是zero-based compressed oops),非常方便;那在这个过程中,我们实际上也只有32位的有效bit去定位内存地址,那何以我们就能使用最高32G的内存呢?这其中可能是有一个trade-off,例如某些大对象我们就能充分利用空出来的8个字节,对于小对象则只能浪费掉了(这一块我目前也没有找到相应的文档,所以也只是猜测)。

When using compressed oops in a 64-bit Java Virtual Machine process, the JVM software asks the operating system to reserve memory for the Java heap starting at virtual address zero. If the operating system supports such a request and can reserve memory for the Java heap at virtual address zero, then zero-based compressed oops are used.

第二个问题是关于为什么是三位而不是四位五位的问题, 压缩后的指针有可能是0x0(二进制0), 0x1(二进制1), 0x2(二进制10), 解压后的指针则是0x0(二进制0), 0x8(二进制1000), 0x10(二进制10000), 中间有8个字节的空档,这个问题的答案和对象对齐有关,在JVM中对象是默认8字节对齐的,所以基本上着正好符合JVM本身的设计,(要注意的是对象对齐本身也会提高内存开销)。

最佳实践

在满足内存和吞吐量的情况下,JVM堆内存设置的越小越好; 如果不能,那么考虑分配的内存满足压缩指针的要求,从而达到优化的效果。