情景
问:Java中的String的字符串长度有限吗?
答:我知道茴的四种写法,你看啊。。。。
问:。。。。
探索
为了了解这问题,我们需要区探究一下class文件。
class文件又叫字节码文件,是它为java实现了跨平台运行的能力。
字节码也解除了虚拟机和java之间的耦合,因为java虚拟机可以支持其他语言上生成的字节码,例如JRuby,Groovy。
从纵观角度来看,class文件只有两种数据结构:无符号数
和表
- 无符号数:属于基本的数据类型,以
u1
,u2
,u4
,u8
来分别代表1字节
、2字节
、4字节
和8字节
的无符号数,他可以用来描述数字、索引引用、数值或者UTF-8字符串编码。 - 表:表是由多个无符号数或者其他表结构组合的复合数据类型,class文件中的所有的表结构都以
_info
结尾。其实class文件就是一张表。
这些结构按照预先规定好的顺序紧密的相连,结构顺序如下所示:
当jvm加载某个class文件时,jvm就是根据上图的结构区解析class文件,并加载到内存中,并根据下图的情况分配内存空间。
用实例分析
首先编写一段简单的java代码
import java.io.Serializable;
public class Test implements Serializable, Cloneable{
private int num = 1;
public int add(int i) {
int j = 10;
num = num + i;
return num;
}
}
然后通过javac生成Test.class,然后用16进制编译器打开:
- 魔数:开头的四位
ca fe ba be
,他是固定值,用来判断是否标准class文件 - 版本号:
00 00 00 34
,前两位代表次版本号(minor_version),后两位代表主版本号(major_version)。说明当前的版本号是52.0对应jdk1.8.0 - 常量池:它是一个叫做常量池的表(cp_info),在常量池中保存了各种类的信息,比如类的名称、父类、方法、参数类型等。
常量池中的每一个类项都是一个表,共有14种类型,如下:
常量池中的每一项都会有一个u1大小的tag值,用于标记当前数据结构属于哪一种表。
我们以CONSTANT_Class_info
和CONSTANT_Utf8_info
两张表举例说明:
table CONSTANT_Class_info {
u1 tag = 7;
u2 name_index;
}
- tag:占用一个字节大小。比如值为 7,说明是
CONSTANT_Class_info
类型表。 - name_index:是一个索引值,可以将它理解为一个指针,指向常量池中索引为 name_index 的常量表。比如 name_index = 2,则它指向常量池中第 2 个常量。
接下来我们来看看CONSTANT_Utf8_info
的表结构:
table CONSTANT_utf8_info {
u1 tag;
u2 length;
u1[] bytes;
}
- tag:值为1 ,表示是
CONSTANT_Utf8_info
表 - length:表示u1[]数组的长度,例如length=5,则表示接下来5个连续的u1类型数据
- bytes:u1 类型数组,长度为上面第 2 个参数 length 的值。
我们java代码声明的String字符串最终的存储格式就是CONSTANT_Utf8_info
,因此length最大能表示的长度就是u2能代表的最大值65536
个,但是需要额外的两个字节来保存null值
,因此String所能表示的最大长度是65536-2=65534
。
不难看出常量池内部的表中也有相互之间的引用,用一张图来表示CONSTANT_Class_info
和CONSTANT_Utf8_info
表格间的关系
理解了常量池内部的数据结构之后,我们看一下实例代码解析过程。从版本号之后开始解析:
00 1d:说明常量计数器的值为29,由于下标为0的常量被JVM使用,我们实际上常量池的大小为28.
** 0a**:可以查到表类型为C
ONSTANT_Methodref_info
,因此常量池中的第一个常量类型为方法引用表。其结构如下:
CONSTANT_Methodref_info {
u1 tag = 10;
u2 class_index; 指向此方法的所属类
u2 name_type_index; 指向此方法的名称和类型
}
- 00 06:指向常量池中的第6个常量
- 00 15:指向常量池中的第21个常量
第一个表已经解读完了,接下来时第二个表
- 09:当前时字段引用表,结构如下
CONSTANT_Fieldref_info{
u1 tag;
u2 class_index; 指向此字段的所属类
u2 name_type_index; 指向此字段的名称和类型
}
- 00 05 :指向常量池中第 5 个常量。
- 00 16:指向常量池中第 22 个常量。
我们已经解析了常量池中的两个常量, 后面的常量解析方法如出一辙,实际上我们可以借助javap命令来查看常量池中的内容
javap -v Test.class
其结果正如我们前面解析的一样,其中下标为21的常量类型为NameAndType
,它的数据结构是
CONSTANT_NameAndType_info{
u1 tag;
u2 name_index; 指向某字段或方法的名称字符串
u2 type_index; 指向某字段或方法的类型字符串
}
- 而下标在 21 的
NameAndType
的name_index
和type_index
分别指向了 13 和 14,也就是“
和” “()V”
。因此最终解析下来常量池中第 1 个常量的解析过程以及最终值如下图所示:
经过仔细分析,我们可以知道常量池中第一个常量保存的是Object中的默认构造方法。
- 常量池之后,是访问标志00 21,它占两个字节,前面的表中有白框标识:它代表了类或者方法的修辞方式,含义如下图所示。
- 00 05:类索引
- 00 06: 父类索引
- 00 02 :接口索引计数器
回顾常量池
类名是Test,父类是Object,我们接着看接口计数器为2 说明下面的4个字节描述了两个接口
- 00 07:常量值为
"Serializable"
- 00 08 :常量值为
"Cloneable"
综上:当前类为 Test 继承自 Object 类,并实现了“Serializable”和“Cloneable”这两个接口。
接下里是字段表
- 00 02 :字段计数器:表示类中声明类两个字段,接着回出现两个字段表的数据结构,字段表数据结构如下:
CONSTANT_Fieldref_info{
u2 access_flags 字段的访问标志
u2 name_index 字段的名称索引(也就是变量名)
u2 descriptor_index 字段的描述索引(也就是变量的类型)
u2 attributes_count 属性计数器
attribute_info
}
- 00 02:字段访问标志,代表是private类型,解析如下图所示:
- 00 09:变量名索引,变量名是num
- 00 0a:变量类型,类型是I,说明是int类型变量
接下来的解析如出一辙,我们说一下注意事项。
字段表集合中不会列出从父类或者父接口中继承而来的字段。
内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表
方法表紧随字段表其后,也是从一个计数器开始
方法表的结构如下:
CONSTANT_Methodref_info{
u2 access_flags; 方法的访问标志
u2 name_index; 指向方法名的索引
u2 descriptor_index; 指向方法类型的索引
u2 attributes_count; 方法属性计数器
attribute_info attributes;
}
访问标志的值如下:
第一个方法是构造方法,我们主要分析一下add方法:
从图中我们可以看出 add 方法的以下字段的具体值:
- access_flags = 00 01 也就是访问权限为 public。
- name_index = 00 11 指向常量池中的第 17 个常量,也就是“add”。
- type_index = 00 12 指向常量池中的第 18 个常量,也即是 (I)。这个方法接收 int 类型参数,并返回 int 类型参数。
属性表:在之前解析字段和方法的时候,在它们的具体结构中我们都能看到有一个叫作 attributes_info 的表,这就是属性表。
属性表并没有一个固定的结构,各种不同的属性只要满足以下结构即可:
CONSTANT_Attribute_info{
u2 name_index;
u2 attribute_length length;
u1[] info;
}
我们接着往下看:
- 00 01:属性计数器,说明只有一个属性
- 00 0f:属性索引,通过查看常量,我们可以看出它是code属性表
code属性表中,最主要的就是一系列的字节码。通过`javap -v Test.class`我们可以查看到方法的字节码
JVM执行add方法时,就是通过这一系列的指令操作来完成的。
解答
String能保存的最大长度需要从两个角度来回答,在编译期还是运行期。
编译期由于是CONSTANT_Utf8_info
格式存储的,所以最大长度是65534
字节,这里需要注意,英文和数字是占用1字节,而汉字是占用两个字节,所以不一定能存到最大长度的字符。
运行期时字符串的内部是有char数组的value来存储的,数组的长度表示类型是int类型,所以这时候String的最大长度是Integer.MAX_VALUE (2147483647)
了,大约运行时需要4BG内存才能达到最大最大字符串长度。