字符串长度限制问题

  • 前言
  • 分析
  • 字符串常量池
  • 运行时的字符串大小
  • 总结



前言

通过阅读JVM规范源码,我们可以知道,String无论是字面量定义的形式还是运行时生成的方式都是有限制的。

Javac(eclipse编译方式可能作了些修改)编译阶段,字面量定义的字符串形式需要小于65535,运行时阶段大概小于2^31,4个G左右。


分析

如图所示,先动态的输出10w个1,然后copy出来,以字面量的形式定义一个字符串s,然后输出,此时会报错。

des java 长字符串 java 常量字符串过长_des java 长字符串


报错原因为字符串过长,即编译阶段,字面量形式定义的字符串是会有限制的。

des java 长字符串 java 常量字符串过长_String_02


有小伙伴可能说为啥我的就没有报错呢,可能这里需要将编译方式修改为javac的方式1。

des java 长字符串 java 常量字符串过长_jvm_03


des java 长字符串 java 常量字符串过长_String_04

字符串常量池

通过阅读java虚拟机规范,我们可以知道字符串常量池的形式如下图表示:

首先,字符串常量池的实现和Map有着异曲同工直面,以哈希表和链表的方式进行实现。其中,tag是一个固定值8(常量池有很多种,每种都有特定数字进行表示)

des java 长字符串 java 常量字符串过长_des java 长字符串_05


而关于string_index则为索引值,指向相应的字符串序列,其结构可以用Constant_Utf8.info进行表示。其中,u2为无符号的2字节,2^15+…+ 2^0=65535。即通过字面量定义的字符串最为65534.

des java 长字符串 java 常量字符串过长_常量池_06


通过对编译时的代码进行调试,我们可以找到一个checkStringConstant方法,在编译阶段,进入常量池的字符串会在这里做成判断,如果大于等于65535,会输出错误日志。

private void checkStringConstant(DiagnosticPosition var1, Object var2) {
        if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
            this.log.error(var1, "limit.string", new Object[0]);
            ++this.nerrs;
        }
    }

运行时的字符串大小

当我们绕过编译器,来到运行时的阶段,我们的字符串大小的限制就远远超过65535了,我们可以看如下的代码,它的输出是没有问题的:

public static void main(String[] args) {
    String s = "";
    for (int i = 0; i < 100000; i++) {
        s+=i;
    }
    System.out.println(s);
   

}

为什么呢?原来通过“+”运行,我们的编译器会优化成:

new StringBuilder().append("XX")...append("XX").toString()

再看看StringBuilder源码中的toString方法,它会调用String的构造方法public String(char value[], int offset, int count)

@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

关于该构造方法,代码如下所示:

public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

看到这里,机制的小伙伴应该发觉原因了吧,其实在StringBuild阶段我们大概就可以猜出来了,因为count表示了字符串的字符个数,而这个count是有长度限制的,它用int进行表示,通过查看它的“装箱类”Integer源码:

/**
     * A constant holding the maximum value an {@code int} can
     * have, 2<sup>31</sup>-1.
     */
    @Native public static final int   MAX_VALUE = 0x7fffffff;

我们可以知道它的最大值为2^31-1,这个数字的字符串大概有多大呢?

首先字符串在其底层,也是用一个char数组进行存储的,而char,一个字符大小相当于2个字节,16位。2^31-1个字符为:

2^31-1 * 16 / 8 /1024 /1024/1024 = 3.99....

大小约等于4G。

总结

字符串的大小限制问题,常见于前后端对接参数时,字符串传参而产生的问题。或者,在对接甲方接口时,你根本不知道它给你响应的东西到底有多个,也不做申明,这时总会产生莫名其妙的问题。