原文地址:http://blog.csdn.net/seatalks/article/details/52981819
在这篇文章中,我们来聊聊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);
编译器实际上要在堆中给这个对象分配比32位多得多的空间。Java中整形数的值是32位,但是每个对象都含有一些“头部”内容。而这些头部内容在不同的VM中是不同,在32位与64位虚拟机中也是不同的。在32位的虚拟机中,头部的每个“域”(field)的长度是一个字长(4字节)。而在64位虚拟机中,整形数的值域长还是32位,但是其他域的长度却增加到了8个字节(64位机中的一个字长)。如果你以为就这点差别的话,我就呵呵了!对象在内存中是要按字对齐的,也就是说,在64位机中,一个对象所占的内存要能被64整除。而我们要做文章主要的地方就是在Hotspot虚拟机中被称为“Klass”的类指针(class pointer)。从下图中,我们可以看到的是,在一个正常的64位虚拟机中klass的长度是8字节,但是启动compressed oops后,它就变成了4字节了。
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<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); } }
在命令行提示符处,先cd到这个文件所在的目录下,然后用下面的命令行来编译它。
javac IntegerApplication.java
现在我们应该得到了一个IntegerApplication.class的文件。接下来,我们运行这个文件两次。第一次打开compressed oops, 第二次关闭。
由于Compressed oops在高于6u32版本的JVM中默认是打开的,所以我们可以直接在命令提示符中键入以下命令运行。
java IntegerApplication
上面的源码中有一个Scanner的对象。它用来保证你的程序能一直运行,除非你键入些东西来终止程序。如果你在命令提示符处看到了“application is running...”,你就可以打开内存分析器了(memory analyzer)。基于你的机器,有可能会要等一会儿,因为它要做一些初始化工作。
从文件菜单中选择“Acquire Heap Dumping...”选项
你可以看到一个进程选择窗口。选择“IntegerApplication”,然后点击“Finish”。
之后,你就可以看到内存分析器的主界面。在工具栏中,选择下图所示位置上的历史分析按钮。
然后,你就可以看到程序中所有对象的信息了。下面是我们这个小例子在compressed oops 打开的情况下的信息。
接下来,我们就关闭compressed oops。为了关闭它,我们可以使用 -XX:-UseCompressedOops 标识。你不需要重新编译你的程序,直接在命令行那里键入下面的命令:
java -XX:-UseCompressedOops IntegerApplication
接下来的步骤跟上面的一样。下面是关闭compressed oops的执行结果:
跟我们想的一样,内存占用增多了。 堆内存主要被两种类型的对象占用了,一种是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并没有什么坏处,并且它带给你的可能还是好处。对于编译器的一些细节的了解可以帮助你写出高效的代码。