欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM规范&源码|字符串长度限制问题

程序员文章站 2022-07-13 21:17:41
...


前言

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

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

分析

如图所示,先动态的输出10w个1,然后copy出来,以字面量的形式定义一个字符串s,然后输出,此时会报错。
JVM规范&源码|字符串长度限制问题
报错原因为字符串过长,即编译阶段,字面量形式定义的字符串是会有限制的。
JVM规范&源码|字符串长度限制问题
有小伙伴可能说为啥我的就没有报错呢,可能这里需要将编译方式修改为javac的方式1。
JVM规范&源码|字符串长度限制问题
JVM规范&源码|字符串长度限制问题

字符串常量池

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

首先,字符串常量池的实现和Map有着异曲同工直面,以哈希表和链表的方式进行实现。其中,tag是一个固定值8(常量池有很多种,每种都有特定数字进行表示)
JVM规范&源码|字符串长度限制问题
而关于string_index则为索引值,指向相应的字符串序列,其结构可以用Constant_Utf8.info进行表示。其中,u2为无符号的2字节,2^15+…+ 2^0=65535。即通过字面量定义的字符串最为65534.
JVM规范&源码|字符串长度限制问题
通过对编译时的代码进行调试,我们可以找到一个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。

总结

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

(完)