如何在Java字符(16位)中存储UTF-8字符(8位)时避免内存浪费。二合一?

时间:2011-04-12 12:02:29

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

我担心我对一个相当过度饱和的主题的细节有疑问,我搜索了很多,但是找不到明确的答案 - 这个特别明显的 - 重要的,问题:

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

是否有一些技巧可以使用8位的伪字符串?这实际上会减少内存消耗吗? 或者,有没有办法存储>两个<一个java 16bit字符中的8位字符可以避免这种内存浪费吗?

感谢任何令人沮丧的答案...

编辑: 嗨,谢谢大家的回答。我知道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是另一个“数据库”的数据库。 我的问题是我必须一直用byte []数据提供jedis,但在内部,> Redis< (实际服务器)只处理“二进制安全”字符串。由于Redis是用C语言编写的,因此char是8位长,AFAIK不是ASCIII,是7位。然而,在Jedis中,java世界,每个字符在内部都是16位长。我还不了解这段代码,但我想jedis然后将这个java 16位字符串转换为符合Redis的8位字符串(([here] [3])。它说它扩展了FilterOutputStream。我希望绕过它字节[]< - >字符串完全转换并使用Filteroutputstream ...?)

现在我想知道:如果我必须一直互换byte []和String,数据量从非常小到可能非常大,那么每个8位字符传递的内存不会浪费大量内存java中的16位?

7 个答案:

答案 0 :(得分:9)

  

是否有一些技巧可以使用8位的伪字符串?

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

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

  

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

编辑:此选项在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

答案 1 :(得分:5)

实际上,您的UTF-8部分是错误的:UTF-8是一个可变长度的多字节编码,所以有效字符长度为1-4个字节(换句话说,一些UTF-8字符是8位有些是16位,有些是24位,有些是32位。虽然1字节字符占用8位,但还有更多的多字节字符。如果你只有1个字节的字符,它只允许你有256个不同的字符(a.k.a。“扩展的ASCII”);这可能足以满足90%的英语使用率(我的天真估计),但只要你甚至认为超出该子集的任何东西,就会咬你的屁股(看到naïve这个词 - 英文,但不能用ASCII编写。)

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

对于字符编码的稍微更长的介绍,请参阅例如这个:http://www.joelonsoftware.com/articles/Unicode.html

答案 2 :(得分:2)

  

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

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

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

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

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

答案 3 :(得分:2)

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

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

它唯一有用的地方是JVM堆。但是,你需要有成千上万的8位字符来注意Java堆使用方面的任何真正差异 - 这些都远远超过了你所做的所有额外(hacky)处理。

RAM中的一百多个8位字符只是“浪费”大约1 MiB ......

答案 4 :(得分:1)

  

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

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

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

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


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

答案 5 :(得分:0)

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

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

https://github.com/ib84/castriba

答案 6 :(得分:-1)

也许这就是你想要的:

// 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 yyyy x为{1},y为char 2.所以这意味着每个字符只有8位。你能用8位做出的最大整数是多少?答案:255

255 = 0000 0000 1111 1111(8位)。当你使用char&gt; 255,那么你会有这个:
256 = 0000 0001 0000 0000(超过8位),不适合您为1个字符提供的8位。

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