问题:String s = new String("i");占用多大内存?
面到这个题目的时候,我实际上不知道到底问的是啥?从小白的角度来看,这不是分三部分吗?
String 类型的引用,String 实例对象,String 类型的字符串
难道是把这三者加起来?可 JVM 那本书上并没有说引用类型占多少字节,String 类型也不属于基本数据类型啊?
本文基于小白水平借助 openjdk.jol 工具复盘该题,主要包含以下三个方面的内容:
① new String() :发生了什么,即本题实际在考察什么的内存占用
② openjdk.jol :通过 JVM 对象布局工具看结果
③ 扩展测试:引用类型和数组类型的内存占用情况
由浅入深:先从基本数据类型占用空间大小分析,也作为基本知识点回顾
基本数据类型
| 数据类型 | boolean | byte | short | char | int | float | long | double |
|---|---|---|---|---|---|---|---|---|
| 占用内存(Byte) | 1 | 1 | 2 | 2 | 4 | 4 | 8 | 8 |
关于 boolean ,有些博客上说是 1/8 字节,有些是 1 字节
因其他数据类型都是 8 byte 的整数倍,对象存储布局中有对齐填充,所以 boolean 这里应该视为 1 字节
深入理解 JVM pg 52 :HotSpot 虚拟机默认的分配顺序为 longs/doubles,ints,shorts/chars,bytes/booleans…,相同宽度的字段总是被分配到一起存放
也即在内存分配时:byte 和 boolean 具有相同的字宽为 1 字节
深入理解 JVM pg 48 : JVM 执行 new 指令时,会判断能否定位到常量池中类的符号引用,主要是为了判断对应的类是否已完成类的加载过程,若没有,会先执行类的加载
类加载完成后,会为新生对象分配内存,即对象所需要内存的大小在类加载完后便可完全确定
再回到题目String s = new String("i");
现在还仅在执行程序的右半部分,执行 new 指令,加载 String 类
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
这段程序后面还用,放在此处的目的主要说明:
String 类有 char[], int, long 类型的三个属性
String 类的属性有一个是 static 的,后面会有说明
深入理解 JVM pg 367 :类加载的准备阶段,进行内存分配的仅包括类变量,而不包括实例变量
深入理解 JVM pg 367 :加载阶段结束后,JVM 外部的二进制字节流按照虚拟机所设定的格式存储在方法区。类型数据安置在方法去后,会在 Java 堆内存中实例化一个 java.lang.Class 类的对象
这里主要说明两点:
① 上述实例字段在类加载过程中尚未分配内存
② 对象头包含实例数据【运行时数据】和类型指针,其中类型指针指向方法区中的对象类型数据
因此 String 对象的内存分配需要从对象的创建开始分析
关于类型数据:个人理解可以是常量池中变量和类的描述信息等信息
例如:
public class TestReference {
public String s1 = new String("a");
public static void main(String[] args) {
String[] t = {"a","b"};
String[] t1 = new String[]{"a","b"};
}
}
截取 javap -v后生成的部分字节码文件
Constant pool:
#1 = Methodref #8.#26 // java/lang/Object."":()V
#2 = Class #27 // java/lang/String
#3 = String #28 // a
#4 = Methodref #2.#29 // java/lang/String."":(Ljava/lang/String;)V
#5 = Fieldref #7.#30 // Test/TestReference.s1:Ljava/lang/String;
#6 = String #31 // b
#7 = Class #32 // Test/TestReference
#8 = Class #33 // java/lang/Object
...
#22 = Utf8 t
#23 = Utf8 t1
深入理解 JVM pg 48 : 类加载后检查通过后,虚拟机将为新生对象分配内存
对象所需内存大小在类加载完成后便可完全确定,分配过程在2.3.2 对象的内存布局
这里主要整理下对象创建的过程,对象内存分配在下一小节展开
① 程序入口 main(…) 执行 new 指令
② 执行类加载过程,方法区中存放类型数据,堆中实例化一个 Class 对象
③ 新生对象分配内存
④ 初始化分配到的内存空间
⑤ 对对象进行必要的设置,例如设置对象头信息等
⑥ 执行 方法,进行对象的初始化
和方法:
深入理解 JVM pg 367 : 和两个构造器的产生实际上是一种代码收敛的过程
即经过编译器,语句块 (实例构造器 {},静态构造器 static {}),变量初始化(实例变量,类变量)、调用父类的实例构造器等操作收敛到 和方法中
需要注意的是 和方法并不是一起执行
深入理解 JVM pg 277 : 初始化阶段就是执行类构造器方法的过程
深入理解 JVM pg 49: new 指令之后会接着执行方法
即:方法在类加载之后创建对象过程中执行,而方法是在类加载的初始化阶段执行
深入理解 JVM pg 51: 对象在堆内存中的存储布局分为三部分:对象头、实例数据和对齐填充
也即:对象内存 = 对象头 + 实例数据 + 对齐填充
对象头:Mark Word + 类型指针
实例数据:可理解为对象的实例变量
对齐填充的作用:保证对象的大小是 8字节的整数倍
上述对象头有必要推敲一下:Mark World,64 位虚拟机占 64 位,即 8 字节
| 存储内容 | 标志位 | 状态 |
|---|---|---|
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
| 空,不需要记录信息 | 11 | GC 标记 |
| 偏向线程 ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
即自身运行时数据部分主要是跟GC,锁,哈希码这些有关,是动态定义的数据结构
现在仅先理解,对象头的中存储对象运行时数据的部分占 8 字节
那么另一部分呢?类型指针占多少字节?
深入理解 JVM pg 53: 对象的访问定位
对象的访问定位分为:通过句柄访问对象,通过指针访问对象
HotSpot 虚拟机采用通过指针访问对象的方式

绘出这个图主要是说明:虚拟机栈中的 reference 和 对象的类型指针并不是指的同一个
Java 虚拟机栈中的 reference 存储的是对象的地址,而对象的类型指针仅是其中的一部分【因笔者之前看图的时候有混淆所以提出来】
那么类型指针部分到底是多少字节呢?首先需要清楚对象类型数据指针的本质
深入理解 JVM pg 295: 虚拟机实现至少都应当通过引用做到两件事,一是根据引用直接或间接地查找到对象在 Java 堆中数据存放的起始地址或索引,二是根据引用直接或间接地查找到对象所属数据类型在方法区中存储的类型信息
也即这里的对象类型数据指针 本质上是 reference类型
那么 reference类型的长度是多少呢?很遗憾还是没找到确切答案
深入理解 JVM pg 295: Java 虚拟机规范中没有明确规定 reference 类型的长度,与实际使用32位/64位虚拟机有关,对于 64 位虚拟机,还与是否开启对象指针压缩的优化有关
不过对于 32 位的虚拟机,reference 类型占位不超过 32 位,【周老师原话是这样说的】
那么实例数据是哪些呢?这部分不好说明,笔者也一知半解,放到使用 jol 工具分析部分进行说明
通过上述 通过直接指针访问对象的图示,可以再回到题目 String s = new String("i");
对 new 指令触发的类加载和创建对象过程梳理下,然后看这个过程到底是对什么的内存分配,是不是还是之前认为的 String 类型的三部分相加
以简单的 demo 为例,方法的入口 Main() 中定义了一个 String 类型的引用指向 String 实例对象,即原题目直接放到 main 中
public class TestReference {
public static void main(String[] args) {
String s1 = new String("i");
}
}
new 指令触发的类加载和创建对象过程如下:
① 执行 main 方法,在虚拟机栈中创建对应的栈帧
② new 指针触发 String 类加载过程,在堆中生成代表该类的 Class 对象,在方法区存放对象的类型数据
③ 执行 方法,进行对象的初始化,即通过构造函数设置String 对象实例变量的值
④ String s1 作为当前 main 栈帧中局部变量表中的一个 reference 类型的变量,该变量指向堆中对象的地址
上面还有些细节,比如还没有说清楚,这里的实例数据是什么,上述代码中(i)怎么理解,似乎作为 String 的实例数据有点尴尬。这个还是通过 openjdk.jol 工具来说明
通过上述,可以得到的结论是:String s1 = new String("i");实际上仅涉及到 String 对象的分配,String 类型的对象引用是在虚拟机栈中分配内存的,方法调用结束后自动回收。所以我们应该着眼的是后面这部分
也即 new String("i")这部分的内存分配情况
了解到 openjdk.jol工具,主要通过下面两篇博客
① 创建 maven 工程
② pom.xml 文件中引入 jol 工具依赖
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.9version>
dependency>
③ 打印实例对象的相关信息
public class TestReferece {
public static void main(String[] args) {
String s = new String("i");
System.out.println("s - headerSize " + ClassLayout.parseInstance(s).headerSize());
System.out.println("s - printable " + ClassLayout.parseInstance(s).toPrintable());
}
}
④ 运行结果
s - headerSize 12
s - printable java.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) da 02 00 20 (11011010 00000010 00000000 00100000) (536871642)
12 4 char[] String.value [i]
16 4 int String.hash 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
运行结果:
结合上述测试结果:
String 实例对象占用分为三部分:12 字节对象头 + 8 字节实例数据 + 4 字节的对齐填充
回头看1.4 对象内存布局,对象头的运行时数据部分占 8 字节,则此处另一部分类型指针占 4 字节
不过回到 1.2 String 类的属性,String 类有三个属性,但是这里实例数据只有两个 char[]和 int
这个问题是临时发现的,笔者也没有想明白
① 上述 demo 的字节码文件中也没有关于 long 类型的 serialVersionUID 的说明
② 深入理解 JVM pg 272:JDK 8 之后,类变量会随着 Class 对象一起存放在 Java 堆中
③ 基于 ② 的理解,static 变量不属于类,虽然放在堆中,但不属于对象的一部分
所以,这里实例数据主要指 char[]和 int类型的变量
之前遗留的问题是:new String("i") 中的 i字符串怎么理解
OFFSET SIZE TYPE DESCRIPTION VALUE
12 4 char[] String.value [i]
上面的结果可以回答之前new String("i") 中的 i变量的本质
从构造函数入手:参数中的 origianl 仍然是 String 对象
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
也即参数i可以追溯到this.value
private final char value[];
而 value 的本质正是 char 类型的数组,i实质上是 original.value中的 char[0]
通过上面的结果,char[] 类型数组占 4 个字节
关于这 4 个字节,可能还需要思考:应该将其视为是引用类型的 4 字节还是 char 类型的 2 字节 + 1个 char 类型的字符 2 字节
扩展测试的目的:
① 对象类型作为实例数据【即实例数据部分的引用类型内存占用】
② 数组类型作为实例数据 【分析上述 char value[] 的 4 字节】
public class A {
private String s1;
private String s2;
}
A a = new A();
System.out.println("A - headerSize : " + ClassLayout.parseInstance(a).headerSize());
System.out.println("A - printable : " + ClassLayout.parseInstance(a).toPrintable());
System.out.println("A.class - printable" + ClassLayout.parseClass(A.class).toPrintable());
打印结果:
A - headerSize : 12
A - printable : MemoryTest.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c3 00 20 (01000011 11000011 00000000 00100000) (536920899)
12 4 java.lang.String A.s1 null
16 4 java.lang.String A.s2 null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
A.class - printableMemoryTest.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 java.lang.String A.s1 N/A
16 4 java.lang.String A.s2 N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
结论:空间占用显示没有区别,VALUE 部分有区别,查看 parseInstance更具有参考意义
public class A {
private String s1;
private String s2;
}
public class B {
private A a;
}
B b = new B();
System.out.println("B - headerSize : " + ClassLayout.parseInstance(b).headerSize());
System.out.println("B - printable : " + ClassLayout.parseInstance(b).toPrintable());
打印结果:
B - headerSize : 12
B - printable : MemoryTest.B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 15 e1 00 20 (00010101 11100001 00000000 00100000) (536928533)
12 4 MemoryTest.A B.a null
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
结论:对象引用做实例数据同 String 类型的引用都占 4 字节
String sb = new String();
System.out.println("sb - headerSize" + ClassLayout.parseInstance(sb).headerSize());
System.out.println("sb - printable" + ClassLayout.parseInstance(sb).toPrintable());
String sb1 = new String("a");
System.out.println("sb1 - headerSize" + ClassLayout.parseInstance(sb1).headerSize());
System.out.println("sb1 - printable" + ClassLayout.parseInstance(sb1).toPrintable());
测试结果:
sb - headerSize12
sb - printablejava.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) da 02 00 20 (11011010 00000010 00000000 00100000) (536871642)
12 4 char[] String.value []
16 4 int String.hash 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
sb1 - headerSize12
sb1 - printablejava.lang.String object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) da 02 00 20 (11011010 00000010 00000000 00100000) (536871642)
12 4 char[] String.value [a]
16 4 int String.hash 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
结论:两端代码的内存分析结果基本上没有区别,只是在关注的 char[] 部分对应的 VALUE 不同:无 String 参数的为 [],有 String 参数的为对应的值 [a]
原因也可与下面关于 char[] 对象的测试对比,这里实例变量的 TYPE DESCRIPTION为 char []即引用类型,也即实际上是引用类型的地址,注意不是数组对象,而仅仅是作为引用类型
说明:以下代码中的数组对象,均声明在 main() 方法中,不作为某个对象的实例变量
char[] cc = new char[1];
System.out.println("cc - headerSize" + ClassLayout.parseInstance(cc).headerSize());
System.out.println("cc - headerSize" + ClassLayout.parseInstance(cc).toPrintable());
char[] cc1 = new char[2];
System.out.println("cc1 - headerSize" + ClassLayout.parseInstance(cc1).headerSize());
System.out.println("cc1 - headerSize" + ClassLayout.parseInstance(cc1).toPrintable());
int[] ii = new int[2];
System.out.println("ii - headerSize" + ClassLayout.parseInstance(ii).headerSize());
System.out.println("ii - headerSize" + ClassLayout.parseInstance(ii).toPrintable());
long[] ll = new long[4];
System.out.println("ll - headerSize" + ClassLayout.parseInstance(ll).headerSize());
System.out.println("ll - headerSize" + ClassLayout.parseInstance(ll).toPrintable());
测试结果:
cc - headerSize16
cc - headerSize[C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 41 00 00 20 (01000001 00000000 00000000 00100000) (536870977)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 2 char [C.<elements> N/A
18 6 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
cc1 - headerSize16
cc1 - headerSize[C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 41 00 00 20 (01000001 00000000 00000000 00100000) (536870977)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 4 char [C.<elements> N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
ii - headerSize16
ii - headerSize[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 20 (01101101 00000001 00000000 00100000) (536871277)
12 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2)
16 8 int [I.<elements> N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
ll - headerSize16
ll - headerSize[J object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) a9 01 00 20 (10101001 00000001 00000000 00100000) (536871337)
12 4 (object header) 04 00 00 00 (00000100 00000000 00000000 00000000) (4)
16 32 long [J.<elements> N/A
Instance size: 48 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
测试结果:
① 数组类型对象的 headerSize = 16
② 实例数据部分的大小 = 声明时分配的大小 n * 数组类型
补充:从这里也可以看出上述 String 中的 char[] value 是作为引用的,还是从 TYPE DESCRIPTION分析
## 数组对象
OFFSET SIZE TYPE DESCRIPTION VALUE
16 4 char [C.<elements> N/A
16 8 int [I.<elements> N/A
16 8 int [I.<elements> N/A
## String 中的 char[] value
12 4 char[] String.value [a]
写在最后:本来今天还有另一篇需要写,计划上午就写完的,结果扩展到下午。
所以短小精悍的博客是值得推崇的,不过推理的过程是有趣的,会发现很多问题,然后推理,最后验证;如此循环
不过最后的结果是,没有气力再去总结,或许我们不应该交给别人一个答案,而是交付做事的方法和分享思路
正如 当幸福来敲门中主角应试时说的:我不知道答案,但我知道怎么做
可以如此吗?道的意义是什么?许三多意味着什么?
结论总结:
① 非数组类型的对象的对象头占 12 字节【64位虚拟机+开启对象指针压缩】
② 对象类型引用类型作为对象的实例数据时占用 4 字节
③ 数组对象占用内存 = 16 字节对象头 + 数组容量 * 数组类型
④ String 对象占用内存 = 12 字节对象头 + 8 字节实例数据 + 4 字节对齐填充