在本文中,我们将向您介绍一种称为Compressed oops的JVM优化。 压缩oop的概念是由32位和64位体系结构之间的差异引起的。 因此,我们将对64位体系结构进行简短的回顾,然后再深入探讨压缩oop的主题。 最后,我们将通过一个简单的示例看到所有这些。
本文的示例代码非常简单,因此我们将不使用任何IDE。 在32位计算机上,压缩oop没有任何意义。 同样,在6u23之前的JDK中,默认情况下未激活它。 因此,我们假设您使用的是比6u23更新的64位JDK。 我们需要的最后一个工具是内存分析器工具。 在此示例中,我们使用了行业标准的Eclipse Memory Analyzer Tool版本1.5。
1. 32位和64位
32位与64位在2000年代初风靡一时。 尽管64位CPU在超级计算机领域并不是什么新鲜事物,但直到最近,个人计算机才将其推向主流。 从32位架构过渡到64位绝非易事,从硬件到操作系统的所有事物都必须改变。 Java通过引入64位虚拟机来拥抱这种转变。
这种过渡的主要优势是存储容量。 在32位系统中,您的内存地址宽度为32位(因此称为名称),这意味着可寻址内存的总量为2 ^ 32或4 GB RAM。 过去,这可能是一台个人计算机的无限内存(毕竟,那些需要超过640kB RAM的内存!),但在拥有1 GB内存的智能手机被认为是低端产品的时候,却不是。 64位体系结构解决了此限制。 在这样的机器中,可寻址内存的理论数量为2 ^ 64,这是一个非常大的数字。 不幸的是,这只是一个理论上的上限,在现实世界中,有很多硬件和软件因素将我们限制在较小的内存上。 例如,Windows 7 Ultimate仅支持最大192GB。 仅将单词用于192 GB似乎有些苛刻,但与2 ^ 64相比,它显得苍白。 现在您已经了解了为什么64位很重要,让我们继续进行下一部分,并了解压缩oop将如何为我们提供帮助。
2.理论上的压缩
“没有免费的午餐之类的东西”。 64位计算机中过多的内存需要付出一定的代价。 通常,一个应用程序在64位系统上会消耗更多的内存,而在非平凡的应用程序中,这个数量是不可忽略的。 压缩的oop通过在64位环境中使用32位类指针来帮助您保留一些内存,前提是您的堆大小不会大于32 GB。 为了更详细地了解这一点,让我们看看如何用Java表示对象。
Java中的对象表示
为了查看Java中对象的表示方式,我们使用一个非常简单的示例,一个持有原始int的Integer对象。 当您编写如下简单的代码行时:
Integer i = new Integer(23);
编译器为此对象分配了超过32位的堆。 在Java中,int的长度为32位,但是每个对象都有标头。 这些标头的大小在32位和64位以及不同的VM中有所不同。 在32位虚拟机中,每个标头字段均为一个字或4个字节。 在64位虚拟机中,保存int的字段保留为32位,但其他字段的大小加倍为8个字节(在64bit环境中为一个字)。 实际上,故事还没有结束。 对象是按字对齐的,这意味着在64位计算机中,它们占用的内存量必须能被64整除。对我们而言,主要关注点是类指针的大小,在Hotspot VM术语中称为Klass。 正如您在下图中所看到的,在64位虚拟机上,klass大小为8个字节,但是启用了压缩oops后,大小变为4个字节。
压缩的oop如何实现
压缩后的oop中的oop表示普通对象指针。 这些对象指针(如上一节所述)与计算机的本机指针大小相同。 因此,在32位和64位计算机上,oops大小分别为32位或64位。 使用压缩的oop,我们在64位计算机上具有32位指针。
压缩的oop背后的技巧是内存的字节寻址和字寻址之间的区别。 使用字节寻址,您可以访问内存中的每个字节,但每个字节也需要一个唯一的地址。 在32位环境中,这会将您限制为2 ^ 32字节的内存。 在字寻址中,您仍然具有相同数量的可寻址存储块,但是此存储块是一个字而不是一个字节。 在64位计算机中,一个字为8个字节。 这为JVM提供了三个零位。 Java通过转移这些位来利用它们,以扩展可寻址内存并实现压缩的oop。
3.压缩的行动
要查看压缩的操作的效果,我们使用一个简单的应用程序。 这是一个小的Java对象,它制作了200万个整数的链表。
为了能够查看堆条件,我们使用Eclipse Memory Analyzer Tool。 由于我们没有使用Eclipse IDE,因此我们使用独立的应用程序。 您可以从这里下载。
由于此示例仅使用一个类,因此我们不使用Eclipse或任何其他IDE。 使用文本编辑器并创建一个名为IntegerApplication.java的文件。 在文件中键入以下代码。 请记住,文件名应与java类的名称匹配。 您可以从本文的下载部分下载类文件,而无需手动输入。
IntegerApplication.java
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);}
}
打开命令提示符窗口,然后导航到该文件的目录。 使用以下命令进行编译。
javac IntegerApplication.java
现在,您应该有一个IntegerApplication.class文件。 我们运行此文件两次,一次启用压缩oop,第二次不使用压缩oop。 在高于6u23的JVM中,默认情况下启用压缩操作,因此您只需要通过在命令提示符下键入以下命令来运行应用程序:
java IntegerApplication
您可能已经注意到源代码中的Scanner对象。 它用于使应用程序保持活动状态,直到您键入某些内容并终止它为止。 如果在命令提示符下看到“应用程序正在运行...”一词,则该启动内存分析器了。 根据您的计算机,它可能需要一段时间才能完成初始化过程。
从文件菜单中选择获取堆转储...
您将看到过程选择窗口。 选择名为IntegerApplication的进程,然后单击“完成”。
一段时间后,您将进入MAT的主屏幕。 从工具栏中选择直方图按钮,如图所示:
现在,您可以看到应用程序中所有对象的详细概述。 这是在启用压缩oop的情况下运行的简单应用程序的直方图。
这次,我们在没有压缩的情况下运行应用程序。 为了禁用压缩的oop,我们使用-XX:-UseCompressedOops标志。 您无需再次重新编译类,只需在命令提示符下键入以下命令:
java -XX:-UseCompressedOops IntegerApplication
同样,当您看到“应用程序正在运行...”文本时,将获得与以前相同的堆转储。 这是应用程序在没有压缩的情况下运行时堆转储的直方图。
正如我们预期的那样,堆大小增加了。 堆的大部分被两种类型的对象占据,即链表节点和整数。 在压缩oop版本中,有超过200万个整数需要3200万个字节,而在非压缩oop版本中则需要4800万个字节。 通过简单的数学运算,我们可以看到这完全符合我们的预测。
2000000 *(128/8)= 32000000或32兆字节
2000000 *(192/8)= 48000000或48兆字节
如果您在第二个方程式中注意到我们使用了192,而在上一节中,对象大小被称为160位。 原因是Java是按字节寻址的,因此地址与最接近的8个字节对齐(在这种情况下为192位)。
4。结论
这里提供的示例是人为设计的,但这并不意味着它在现实世界的应用程序中不成立。 与H2数据库应用程序一起测试时,压缩的oop将堆大小从3.6兆字节减少到了3.1兆字节。 这将宝贵堆空间的使用效率提高了近14%。 如我们所见,使用压缩的oop并没有什么危害,实际上,大多数情况下,您不会禁用此功能。 但是,了解编译器技巧的细节可以帮助编写考虑性能的代码。
下载源代码
这是看到压缩的oops效果起作用的示例。
您可以在此处下载此示例中使用的IntegerApplication类的完整代码: IntegerApplication
翻译自: https://www.javacodegeeks.com/2016/05/compressedoops-introduction-compressed-references-java.html