CompressedOops: Java中compressed references介绍

本文探讨了CompressedOops(压缩了的普通对象指针)的概念及其在64位JVM中的应用,通过实例展示了如何利用这一特性减少内存占用。

[原文地址] 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);

编译器实际上要在堆中给这个对象分配比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字节了。

Representation of an Integer object in different VMs

                         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<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...”选项。

Process selection window

                                      Process selection window

你可以看到一个进程选择窗口。选择“IntegerApplication”,然后点击“Finish”。

之后,你就可以看到内存分析器的主界面。在工具栏中,选择下图所示位置上的历史分析按钮。

Select Histogram from toolbar

                                   Select Histogram from toolbar

然后,你就可以看到程序中所有对象的信息了。下面是我们这个小例子在compressed oops 打开的情况下的信息。

Heap dump of application with compressed oops enabled.

         Heap dump of application with compressed oops enabled.


接下来,我们就关闭compressed oops。为了关闭它,我们可以使用 -XX:-UseCompressedOops 标识。你不需要重新编译你的程序,直接在命令行那里键入下面的命令:

java -XX:-UseCompressedOops IntegerApplication

接下来的 步骤跟上面的一样。下面是关闭compressed oops的执行结果: http://www.javacodegeeks.com/wp-coheapDumpNotCompressed

                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并没有什么坏处,并且它带给你的可能还是好处。对于编译器的一些细节的了解可以帮助你写出高效的代码。







<think> 我们注意到用户的问题是关于Windows系统打包时出现的OodleDecode错误。错误信息为:"compressed buffer starts with zero byte; invalid or corrupt compressed stream"。这表明在解压缩过程中遇到了以零字节开头的压缩缓冲区,意味着压缩流无效或损坏。 结合用户提供的引用,引用[1]提到目标镜像必须被压缩(仅限qcow/qcow2和vmdk的streamOptimized子格式),这可能暗示压缩格式的问题。但用户的问题发生在Windows打包过程中,可能与游戏开发或软件打包相关,而不是虚拟机镜像。因此,我们需要从打包过程中使用的压缩方法入手。 Oodle是一种数据压缩库,常用于游戏开发中压缩资源。错误信息明确指出压缩流以零字节开头,这通常表示: 1. 压缩数据被意外截断或未完全写入。 2. 压缩缓冲区未正确初始化或传递了空缓冲区。 3. 使用了不匹配的压缩/解压缩方法(例如,用Oodle压缩但用其他方法解压,或者Oodle版本不一致)。 解决步骤: 1. **检查压缩过程**:确保在打包过程中压缩数据被正确生成。如果使用Oodle压缩,检查是否有错误在压缩时被忽略。例如,检查压缩函数的返回值。 2. **验证压缩数据**:在打包前,可以尝试解压生成的压缩数据,验证其正确性。如果解压失败,则问题出在压缩阶段。 3. **检查文件写入**:确保文件被完整写入,没有因磁盘空间不足、权限问题或进程中断导致写入不完整。 4. **检查Oodle版本**:确保打包工具和运行时使用的Oodle动态库版本一致。版本不一致可能导致压缩格式不兼容。 5. **调试代码**:如果打包工具是自研的,检查调用Oodle压缩的代码。确保传入的缓冲区有效,且压缩后检查返回的压缩大小。特别注意,如果压缩函数返回0,表示压缩失败,此时不应该写入空数据。 6. **检查文件损坏**:如果打包过程中涉及网络传输或磁盘操作,确保文件没有损坏。可以计算并校验文件的哈希值。 7. **查看日志**:检查打包工具或应用程序的日志,看是否有相关的警告或错误信息。 8. **更新工具和库**:如果使用的是第三方打包工具,尝试更新到最新版本,可能该问题已被修复。 由于用户没有提供具体的打包工具或上下文,我们给出一般性建议: - 如果是使用Unreal Engine等游戏引擎打包,尝试清理中间文件(如Intermediate目录)和重新生成项目文件。 - 确保引擎中使用的Oodle插件是最新的,并且与运行时版本匹配。 根据经验,此错误通常由以下原因引起: - 压缩数据为空(大小为0),导致解压器读取到零字节开头。 - 压缩数据在传输或存储过程中被破坏。 因此,重点检查压缩数据的生成和存储过程。 **注意**:用户提供的引用[3]是一个YOLO训练脚本,与当前问题无关。引用[2]是硬件启动信息,也不相关。引用[1]提及压缩,但针对虚拟机镜像格式,可能不直接适用,但原理类似(压缩格式必须正确)。 综上所述,建议用户按照以下步骤排查: 1. 确认打包过程中压缩步骤是否成功完成。 2. 检查压缩后的文件大小是否大于0,并尝试手动解压(如果可能)。 3. 更新Oodle库到最新版本。 4. 检查磁盘空间和权限问题。 5. 如果问题持续,尝试在打包工具中禁用Oodle压缩,看是否还有其他错误。 如果用户能提供更多上下文(如打包工具、是否自研工具、使用的Oodle版本等),可以给出更具体的建议。 相关问题: 1. Oodle压缩在不同版本间的兼容性如何? 2. 如何验证压缩数据的完整性? 3. 在Windows打包过程中如何调试文件损坏问题? </think> ### Windows系统打包时出现 "OodleDecode: compressed buffer starts with zero byte; invalid or corrupt compressed stream" 错误的解决方法 该错误表明 Oodle 压缩库在解压时检测到无效的压缩数据流(以零字节开头)。以下是系统性的解决方案: #### 1. **检查压缩数据源** - **验证源文件完整性**:确保被压缩的文件未损坏或部分缺失。可通过哈希校验(如 SHA-256)对比原始文件。 - **禁用空文件压缩**:若压缩流包含空文件(0 Byte 大小),Oodle 会报此错误。在打包脚本中添加过滤逻辑跳过空文件: ```powershell # PowerShell示例:跳过空文件 Get-ChildItem -Path ./source | Where-Object { $_.Length -gt 0 } | Compress-Archive ... ``` #### 2. **调整压缩参数** - **显式指定压缩级别**:避免使用不兼容的压缩级别。Oodle 推荐级别为 1-9(默认为 8),尝试显式设置: ```python # 在压缩代码中添加(示例) compression_level = 5 # 测试常用级别 5 或 6 ``` - **关闭分块压缩**:若使用分块压缩(chunked compression),尝试禁用此功能,因零字节可能出现在分块边界。 #### 3. **更新/替换压缩库** - **升级 Oodle 版本**:旧版本可能存在已知缺陷。从 [RAD Game Tools 官网](https://www.radgametools.com/oodle.htm) 获取最新库文件。 - **切换压缩算法**:临时改用兼容性更高的算法(如 zlib)测试: ```python # Python zlib 示例 import zlib compressed_data = zlib.compress(original_data, level=5) ``` #### 4. **调试压缩流程** - **记录压缩日志**:在压缩代码中添加详细日志,定位具体触发文件: ```python for file in file_list: if len(file.data) == 0: print(f"警告: 跳过空文件 {file.name}") # 空文件检查 else: compressed = oodle_compress(file.data) if compressed.startswith(b'\x00'): print(f"错误: {file.name} 压缩后首字节为0") # 异常检测 ``` - **十六进制验证**:使用工具(如 HxD)检查压缩后的文件,确认首字节是否为 `00`。 #### 5. **系统与环境修复** - **清理临时文件**:删除打包缓存(如 `%TEMP%` 和项目 `Intermediate` 目录),避免旧数据干扰。 - **禁用防病毒软件**:实时扫描可能中断压缩过程(如 McAfee 或 Norton),测试时临时关闭。 - **检查磁盘错误**:运行 `chkdsk /f` 修复可能的存储介质损坏。 #### 6. **针对引用[1]的特别建议** 若涉及虚拟机镜像(如 qcow2/vmdk),确保压缩时使用 **streamOptimized** 格式并验证镜像完整性: ```bash qemu-img check image.qcow2 # 检查镜像结构 ``` > **操作优先级**: > 1. 跳过空文件 → 2. 更新 Oodle 库 → 3. 调整压缩级别 → 4. 检查磁盘错误 > 多数情况下,显式过滤空文件或升级库可解决该问题[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值