【问题标题】:how to avoid memory wastage when storing UTF-8 characters (8 bit) in Java character (16 bit). two in one?在 Java 字符(16 位)中存储 UTF-8 字符(8 位)时如何避免内存浪费。二合一?
【发布时间】:2011-04-12 12:02:28
【问题描述】:

恐怕我对一个相当过饱和的主题的细节有疑问,我搜索了很多,但找不到一个明确的答案来解决这个特定的明显-imho-重要的问题:

使用UTF-8将byte[]转换为String时,每个字节(8bit)都变成了UTF-8编码的8位字符,但是java中每个UTF-8字符都保存为16位字符。那是对的吗? 如果是,这意味着每个愚蠢的 java 字符只使用前 8 位,并消耗双倍的内存?这也正确吗?我想知道这种浪费行为是如何被接受的......

是不是有一些技巧来拥有一个 8 位的伪字符串?这实际上会导致更少的内存消耗吗? 或者,有没有办法在一个java 16位字符中存储>两个

感谢您提供任何令人困惑的答案...

编辑: 你好,谢谢大家的回答。我知道 UTF-8 的可变长度属性。但是,由于我的源是 8 位字节,我理解(显然是错误的)它只需要 8 位 UTF-8 字。 UTF-8 转换是否实际上保存了您在 CLI 上执行“cat somebinary”时看到的奇怪符号?我认为 UTF-8 只是以某种方式用于将字节的每个可能的 8 位字映射到 UTF-8 的一个特定的 8 位字。错误的?我考虑过使用 Base64,但它很糟糕,因为它只使用 7 位..

问题重新表述:有没有更聪明的方法将字节转换为字符串? 可能最喜欢的是将 byte[] 转换为 char[],但我仍然有 16 位字。

其他用例信息:

我将Jedis(NoSQL Redis 的 java 客户端)改编为 hypergraphDB 的“原始存储层”。所以,jedis是另一个“数据库”的数据库。 我的问题是我必须一直为 jedis 提供 byte[] 数据,但在内部,>Redis 字符串转换并使用那个Filteroutputstream...?)

现在我想知道:如果我必须一直相互转换 byte[] 和 String,数据大小从非常小到可能非常大,将每个 8 位字符传递为java中的16位?

【问题讨论】:

  • 您知道某些 UTF-8 字符是 2、3 或 4 个字节,对吧?全世界都不用ASCII。
  • 嗨,谢谢大家的回答。我知道 UTF-8 的可变长度属性。但是,由于我的源是 8 位字节,我知道它只需要 8 位 UTF-8 字。不是这样吗? UTF-8 转换是否实际上保存了您在 CLI 上执行“cat somebinary”时看到的奇怪符号?我认为 UTF-8 只是以某种方式用于将字节的每个可能的 8 位字映射到 UTF-8 的一个 8 位字。错误的?我考虑过使用 Base64,但它很糟糕,因为它只使用 7 位..
  • UTF-16 也是一种可变宽度编码,就像 UTF-8 一样。它只是使用更大的代码单元。
  • 将两个字节转换为一个字符有什么不好?
  • 您可以对其进行 Huffman 编码,以便最常见的代码点占用最少的位,反之亦然。因此,每个文档都需要一个不同的前置查找字典,将位映射到代码点。这将是一种虚假的效率。

标签: java memory utf-8 byte 8-bit


【解决方案1】:

8位伪字符串不是有什么技巧吗?

是的,请确保您拥有最新版本的 Java。 ;)

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

-XX:+UseCompressedStrings 对可以表示为纯 ASCII 的字符串使用 byte[]。 (在 Java 6 Update 21 Performance Release 中引入)

编辑:此选项在 Java 6 更新 22 中不起作用,并且默认情况下在 Java 6 更新 24 中未启用。注意:此选项似乎可能会使性能降低约 10%。

下面的程序

public static void main(String... args) throws IOException {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < 10000; i++)
        sb.append(i);

    for (int j = 0; j < 10; j++)
        test(sb, j >= 2);
}

private static void test(StringBuilder sb, boolean print) {
    List<String> strings = new ArrayList<String>();
    forceGC();
    long free = Runtime.getRuntime().freeMemory();

    long size = 0;
    for (int i = 0; i < 100; i++) {
        final String s = "" + sb + i;
        strings.add(s);
        size += s.length();
    }
    forceGC();
    long used = free - Runtime.getRuntime().freeMemory();
    if (print)
        System.out.println("Bytes per character is " + (double) used / size);
}

private static void forceGC() {
    try {
        System.gc();
        Thread.sleep(250);
        System.gc();
        Thread.sleep(250);
    } catch (InterruptedException e) {
        throw new AssertionError(e);
    }
}

默认打印

Bytes per character is 2.0013668655941212
Bytes per character is 2.0013668655941212
Bytes per character is 2.0013606946433575
Bytes per character is 2.0013668655941212

使用选项-XX:+UseCompressedStrings

Bytes per character is 1.0014671435440285
Bytes per character is 1.0014671435440285
Bytes per character is 1.0014609725932648
Bytes per character is 1.0014671435440285

【讨论】:

  • 彼得,这非常很有趣,谢谢!我这么说是因为我一直对 Java 的 String 类的 um, finality 以及它与语言中的 String 文字的联系方式感到沮丧。这使得无法对任何类型的改进或变体字符串使用适当的 OO 技术。您刚刚展示了一种这样的用途;我的几个涉及由代码点序列或字素序列组成的假设字符串。我不知道-XX:+UseCompressedStrings;它的存在表明它可能会做其他类似的事情,尽管可能不是便携的。
  • 非常好!如果我节省 50% 的内存,拥有更多内存应该会过度补偿 10% 的性能下降。谢谢
  • 这显示了 Java 语言中用户定义的 String 数据类型的另一个潜在用途。可以根据目的和代码点分布将底层内存分配从可变宽度 UTF-16 更改为可变宽度 UTF-8 或固定宽度 UTF-32。这些可以进一步压缩为一个实施细节——在时间-空间的权衡下。只需要增强 Java String 定义以允许任何符合 “统一代码点接口”的内容。 字符串文字可以由词法范围的编译器指令( pragma) 每个源单元。
  • 我的长评论说明了为什么即使是纯英文文本 ASCII 也是不够的:我不仅使用了 3 种不同类型的引号 (\p{Quotation_Mark}),而且还使用了 4 种不同类型的连字符和破折号 (@987654332 @)。您可能甚至没有注意到哪些是哪些,但没关系:仍然需要它们来正确呈现日常英语。
  • @tchrist,在英格兰,他们在日常英语中使用 £。 ;)
【解决方案2】:

其实你把UTF-8部分搞错了:UTF-8是变长多字节编码,所以有1-4个字节长度的有效字符(换句话说,有些UTF-8字符是8位的,有些是 16 位的,有些是 24 位的,有些是 32 位的)。虽然 1 字节字符占用 8 位,但还有更多的多字节字符。如果您只有 1 个字节的字符,则总共只能有 256 个不同的字符(又名“扩展 ASCII”);这可能足以满足 90% 的英语使用(我的 naïve 猜测),但一旦你甚至 想到 超出该子集的任何内容(看到 naïve 这个词 - 英语,但不能只用 ASCII 写)。

因此,尽管 UTF-16(Java 使用的)看起来很浪费,但实际上并非如此。无论如何,除非你在一个非常有限的嵌入式系统上(在这种情况下,你在用 Java 做什么?),试图减少字符串是毫无意义的微优化。

有关字符编码的稍长介绍,请参见例如这个:http://www.joelonsoftware.com/articles/Unicode.html

【讨论】:

  • @Martijn Courteaux:[流口水,惊呆了]所以你给他看了别的东西,这是错误的,但似乎有效,哪些是非常难以忘怀的?我很震惊,震惊
  • 感谢 piskvor 的回答。我现在正在阅读您的链接。我将 256 个可能的字节字映射到仅 UTF-8 的 8 位 256 个“字符”。所以现在,我更加确信这真的是一件很愚蠢的事情,这种 byte[] string 之间的转换……没有更好的办法了吗??
  • Java 不使用 UCS-2。它使用 UTF-16。这很容易证明:正则表达式引擎将任何 Unicode 字符视为.,无论它占用多少代码单元。
  • @user703862:不是这样。在 UTF-8 中,只有字符 0-127 是单字节的;除此之外的任何东西都是多字节的。尽管 Unicode 的所有编码(UTF-8、UCS-2 和其他)都有其缺点,但是是的,这是最普遍支持的方式。
  • @Piskvor,没问题。我实际工作的一部分包括让我的 Java 语言同事了解 UCS-2 和 UTF-16 之间的差异。大部分问题源于旧文档和“现实的简化版本”。 Java v1 于 95 年首次亮相,Unicode v2 于 96 年首次亮相,但到那时,修复 Java char 错误在政治上已经为时已晚。其他因素共同导致混淆继续存在(例如,接口在处理代码点方面存在缺陷),但没有比 Java charCharacter 太小而无法容纳 Unicode 代码点导致的认知失调更糟糕的了。
【解决方案3】:

当使用 UTF-8 将 byte[] 转换为 String 时,每个字节(8bit)变成一个 UTF-8 编码的 8 位字符

没有。使用 UTF-8 将 byte[] 转换为 String 时,每个 1-6 字节的 UTF-8 sequence 都将转换为 1-2 的 UTF-16 sequence 16 位字符。

在几乎所有情况下,全球,这个 UTF-16 序列都包含一个字符。

在西欧和北美,对于大多数文本,仅使用了这个 16 位字符中的 8 位。但是,如果您有欧元符号,则需要超过 8 位。

有关详细信息,请参阅Unicode。或Joel Spolsky's article

【讨论】:

  • +1 确实。只是一个小小的挑剔:只有当“西欧”是指“英国”而“北美”是指“美国”时,上述内容才成立。否则,您将得到法语é、西班牙语Ñ、德语ß,它们都是UTF-8 中的多字节。
  • @Piskvor:弯引号和适当的破折号和连字符——两者都是正确书写英语所必需的——都采用 UTF-8 中的多个代码单元。至于英国,威尔士语也使用 ASCII 范围之外的字母。此外,任何在 resume 上写 resume 的人都会自动不及格。
  • @tchrist: 确实如此 - 但 1) 请注意 @Anon 写“对于大多数文本”,以及 2) 这些多年来一直(不公平地)被视为“不必要的吹毛求疵,你为什么不能只需使用- 而不是您花哨的 e[mn] 破折号,yada yada”,所以我想展示必要性明确的示例。
  • @Piskvor - 我从来没有说过他们不是。我确实说过大多数西方字符会占据 16 位 Java 字符的低 8 位。而且虽然我没有查过,但我相信你提到的所有字符都是ISO-8859-1,所以这是真的。但是,tchrist 提到的标点符号不适合该空格。
  • ISO 8859‑15,有时称为 Latin‑9, 在某些西方语言中比原始 ISO 8859‑1,Latin‑1, 因为它包括正确书写法语 œuf (U+153) 和 Œuvre de 等单词所需的 œŒ secours aux enfants (U+152) 和 Pierre Louÿs 的ᴀʟʟᴄᴀᴘs 版本,因为 LOUŸS 在 U+178 处需要 Ÿ。但是,这样做的代价是某些其他语言无法接受的。没有 8 位曲目足以编写现代文本,尤其是但只有英语。 Unicode 解决了所有这些问题;请不要重新实例化它们。
【解决方案4】:

Java 在内部将其所有“字符”存储为值的两个字节表示形式。但是,它们的存储方式与 UTF-8 不同。例如,支持的最大值是“\uFFFF”(十六进制 FFFF,十进制 65536),或 11111111 11111111 二进制(两个字节) - 但这将是磁盘上的 3 字节 Unicode 字符。

唯一可能的浪费是内存中真正的“单”字节字符(大多数 ASCII“语言”字符实际上适合 7 位)。当字符写入磁盘时,它们无论如何都会采用指定的编码(因此 UTF-8 单字节字符只会占用一个字节)。

唯一不同的地方是 JVM 堆。但是,您必须拥有成千上万个 8 位字符才能注意到 Java 堆使用的任何真正差异——这将远远超过您所做的所有额外(hacky)处理。

无论如何,RAM 中的一百万个 8 位字符只会“浪费”大约 1 MiB...

【讨论】:

  • +1,轻微的挑剔:没有“存储的 Unicode”之类的东西,但是有几种 Unicode 编码(有点抽象的 字符 和它们的 字节之间的映射陈述)
  • 是的,这可能应该读作“与 UTF-8 存储方式不同”。 \uFFFF 在磁盘 (UTF-8) 上为 11101111 10111111 10111111。
  • 感谢米卡维利。我唯一担心的是 JVM 堆内存。我正在处理大量字节 [],我必须以内存有效的方式将它们打包到“字符串”中。
  • 这是可以理解的,但我不想放弃 Java 的原生 Unicode 支持——还有很多其他变通方法可用于控制内存中字节 [] / 字符的数量(流、持久性等) )。
  • 我不想“放弃”Java 的“原生 Unicode 支持”;但是,我想让它真正起作用。现在有点吝啬和笨拙,这使得它做错事太容易而做正确的事太难了。 Unicode 不只是将字符分配给序数;那是UCS。 Unicode 也是一个丰富的行为集合,Java 几乎没有一个能跟上它的速度。它根本不做完整的大小写映射,虽然它提供了 UAX#15,但它忽略了 UTS#10、UAX#14、UTS#18、UAX#11、UAX#29,实际上大多数关键的 UAX# 44. Java 对 Unicode 的支持很差。
【解决方案5】:

Redis(实际的服务器)只处理“二进制安全”字符串。

我认为这意味着您可以对键/值使用任意八位字节序列。如果您可以使用任何 C char 序列而不考虑字符编码,那么 Java 中的等价物就是 byte 类型。

Java 中的字符串隐含为UTF-16。我的意思是,您可以在其中粘贴任意数字,但该类的目的是表示 Unicode 字符数据。执行byte-to-char 转换的方法执行从已知编码到 UTF-16 的转码操作。

如果 Jedis 将键/值视为 UTF-8,那么它不会支持 Redis 支持的每个值。 并非每个字节序列都是有效的 UTF-8,因此无法使用编码用于二进制安全字符串。


UTF-8 或 UTF-16 是否消耗更多内存取决于数据 - 例如欧元符号 (€) 在 UTF-8 中消耗三个字节,而在 UTF-16 中仅消耗两个字节。

【讨论】:

    【解决方案6】:

    为了记录,我编写了自己的字节[] 字符串互转换器的小实现,它通过将每 2 个字节转换为 1 个字符来工作。它大约快 30-40% 并且消耗(可能少于)Java 标准方式的一半内存:new String(somebyte) 和 someString.getBytes()。

    但是,它与现有的字符串编码字节或字节编码字符串不兼容。此外,在共享数据上从不同的 JVM 调用该方法是不安全的。

    https://github.com/ib84/castriba

    【讨论】:

      【解决方案7】:

      也许这就是你想要的:

      // Store them into the 16 bit datatype.
      char c1_8bit = 'a';
      char c2_8bit = 'h';
      char two_chars = (c1_8bit << 8) + c2_8bit;
      
      // extract them
      char c1_8bit = two_chars >> 8;
      char c2_8bit = two_chars & 0xFF;
      

      当然,这个技巧只适用于 ASCII 字符([0-255] 范围内的字符)。为什么? 因为您想以这种方式存储字符:
      xxxx xxxx yyyy yyyyx 是字符 1,y 是字符 2。所以这意味着每个字符只有 8 位。用 8 位可以得到的最大整数是多少?答案:255

      255 = 0000 0000 1111 1111(8 位)。当您使用大于 255 的字符时,您将拥有:
      256 = 0000 0001 0000 0000(超过 8 位),这不适合您为 1 个字符提供的 8 位。

      另外:请记住,Java 是一种由聪明人开发的语言。他们知道他们在哪里做什么。 推动 Java API

      【讨论】:

      • 看似聪明,但请尝试使用以下两个字符:çé。哦,它们不是 8 位的,是吗?恭喜,您现在在two_chars 中有一个奇怪的混乱,无法提取原始字符。 (有趣的是,每个人都认为 ASCII 应该对每个人都足够了,即使他们每天遇到 ASCII 以外的字符)
      • @Piskvor:我说我知道它不适用于 [0-255] 范围之外的字符。但如果他知道自己在做什么,并且他的应用程序只使用 ASCII 字符,这就是他想要的......
      • 如果 Java 人这么聪明,他们为什么要创建一个不足以容纳字符的 char 数据类型?请记住:“int 是新的 char。”
      • "如果他的应用程序只使用 ASCII 字符" - 这是可能的,但不太可能。在这种情况下,您的方法将是一种有效的方法(尽管我仍然认为有更好的方法来节省空间,例如霍夫曼编码)。
      • 感谢您的回答。我根本不关心角色。我只想有效地将​​字节打包在字符串中。所以是的,字节应该是 [0-255] 也许我必须像你一样使用 Shift 运算符。您的提示让我想起了有些人只是将字节转换为字符串。所以现在我正在尝试将两个字节转换为一个字符。
      猜你喜欢
      • 2021-02-07
      • 1970-01-01
      • 2016-02-06
      • 1970-01-01
      • 1970-01-01
      • 2011-04-26
      • 2019-07-31
      • 2012-02-22
      • 2016-05-31
      相关资源
      最近更新 更多