[原文地址] https://www.javacodegeeks.com/2016/05/compressedoops-introduction-compressed-references-java.html
在这篇文章中,我们来聊聊Compressed oops(压缩了的普通对象指针)。它是JVM的优化技术之一。为什么要提出compressed oops的概念呢?那是因为32位与64位的架构不同导致的。接下来,我们先简单回顾下64位的架构特征,然后再进一步深入地来讨论compressed oops。最后,我们通过 一个小例子来观察它的作用。由于这个小例子十分简单,所以我们不用任何IDE帮忙来写它的代码。
实际上Compressed oops在32位机上是不起任何作用的,并且在JDK6u23之前的版本中,它都默认是被关闭的。所以在这篇文章中我们用的是64位JDK并且版本比6u23版本高。实验中,我们只用到一个内存分析工具——industry standard Eclipse Memory Analyzer Tool (版本1.5).
1. 32位 vs. 64位
32位与64位的对比是在2000年以后兴起的。然而64位CPU早就在超级计算机领域中得到应用了,只是最近几年64位CPU才在PC上成为主流配置。从32位到64位的转变,绝对不是一件简单的工作,因为几乎所有的东西,从硬件到操作系统都必须要发生改变。Java也是在这个改革的趋势中引入了64位的虚拟机。
在32位到64位的转变中,人们最大的获益是内存容量。在一个32位的系统中,内存地址的宽度就是32位,这就意味着,我们最大能获取的内存空间是2^32(或者4 G)字节。这个容量如果放在个人电脑只有640KB内存的时代,那简直就好像是无穷大一样。但是对于现在连一个有1G内存的手机都算是低配的今天,我们就不能这么认为了!在一个64位的机器中,理论上,我们能获取到的内存容量是2^64字节,这是一个十分庞大的数字(ridiculously huge number)。可惜的是,这只是一个理论值,而现实中,因为有一堆有关硬件和软件的因素限制,我们能得到的内存要少得多。举个例了来说,Windows 7 Ultimate系统只能支持192GB的内存。可能许多人会说“192GB好大呀”,但是和2^64比起来,它真的挺小的,真的。好了,我们聊了半天,64位的重要性我们也知道了,那么接下来,我们就谈谈compressed oops能帮我们做什么。
2. Compressed oops 的概述
“天下没有免费的午餐”。我们在64位机器中能获取极大的内存容量也是有花费的。一般来说,一个应用跑在64位机上会花费更多的内存。而且如果不是那种可以乎略不记的小程序的话,这种花费可是不能被忽略不记的哦!而Compressed oops是在64位环境中使用32位类指针(class pointer),这样就可以帮助我们节省一些内存空间,但是要保证内存不大于32GB。接下来,我们细说说一个对象在Java里是如何表示的。
2.1. Java中的对象表示
我们先用一个小例子来帮助我们理解对象(objects)在Java中是如何表示的。我们先给一个Integer对象赋一个值。当你写出下面的这句代码时:
Integer i = new Integer(23);
Representation of an Integer object in different VMs
2.2. compressed oops的实现
oop其实代表的是普通对象指针(ordinary object pointer)。这些对象指针和机器的本地指针的长度是一样长的,所以 oops 在32位机上是32位长,在64位机上是64位长。但是在compressed oops中我们可以在64位机器上使用32位长的指针。
compressed oops的关键就在于内存是按字节编址,还是按字编址。如果按字节编址,我们可以获取到内存中每个字节的内容,但是也需要对每个字节编址。在32位的环境中,这会限制你只有2^32的字节内存。但是如果用字编址的话,你还是可以访问这么多的内存块,但是每个内存块现在是一个字而不再是一个字节。在64位机中,一个字就是8个字节。这就会让JVM的地址最后三位为 0 。Java就利用通过移动(shifting)三位来达到扩大内存的并且实现compressed oops的目的。
3. Compressed oops的执行
为了看compressed oops的执行效果,我们来写一个简单的应用。我们使用一个LinkedList对象 list 200万个整形对象。
为了达到查看堆状态的目的,我们使用Eclipse Memory Analyzer Tool来分析结果。
既然这个例子中,我们不用Eclipse或其他的IDE,所以我们用一个文本编辑器创建一个名叫IntegerApplication.java的文件。把下面的代码敲进这个文件中。注意,文件名与java class的名字要一致!
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
public class IntegerApplication {
public static void main(String[] args) {
List 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);
}
}
在命令行提示符处,先cd到这个文件所在的目录下,然后用下面的命令行来编译它。
javac IntegerApplication.java
现在我们应该得到了一个IntegerApplication.class的文件。接下来,我们运行这个文件两次。第一次打开compressed oops, 第二次关闭。
由于Compressed oops在高于6u32版本的JVM中默认是打开的,所以我们可以直接在命令提示符中键入以下命令运行。
java IntegerApplication
从文件菜单中选择“Acquire Heap Dumping...”选项。
Process selection window
你可以看到一个进程选择窗口。选择“IntegerApplication”,然后点击“Finish”。
之后,你就可以看到内存分析器的主界面。在工具栏中,选择下图所示位置上的历史分析按钮。
Select Histogram from toolbar
然后,你就可以看到程序中所有对象的信息了。下面是我们这个小例子在compressed oops 打开的情况下的信息。
Heap dump of application with compressed oops enabled.
接下来,我们就关闭compressed oops。为了关闭它,我们可以使用 -XX:-UseCompressedOops 标识。你不需要重新编译你的程序,直接在命令行那里键入下面的命令:
java -XX:-UseCompressedOops IntegerApplication
Heap Dump of application with Compressed oops disabled
跟我们想的一样,内存占用增多了。 堆内存主要被两种类型的对象占用了,一种是list nodes,另一种是integers。200万个整形数在compressed oops的环境中需要3200万字节的空间,而把compressed oops关闭后,就需要4800万字节空间。这一个简单的小例子的执行结果,跟我们预期的一样。
2000000*(128/8) = 32000000 or 32 megabytes
2000000*(192/8) = 48000000 or 48 megabytes
如果仔细看下第二个等式,我们用了192而不是之前介绍java对象时图中的160。原因是,Java是按字节编址的,所以地址要跟最近的8字节对齐,这里的话就是192位了。
4. 总结
这里提供的例子可能有些差强人意,但是它很能反映实际情况。如果用H2数据库应用作测试用例,compressed oops可以把堆大小从3.6MB减小至3.1MB。这也意味着多了差不多14%可用的堆。显而易见,使用compressed oops并没有什么坏处,并且它带给你的可能还是好处。对于编译器的一些细节的了解可以帮助你写出高效的代码。