本文的内容来自《算法 第四版》,上次看这一部分的时候应该是一年前了,不过因为昨天在面试中,被问到String对象在内存是如何存储,虽然之前看过这方面的内容,对这还有一点模糊印象,但终究没想起来,所以就想再看一下,顺便写成博客,方便以后查看。

本文主要介绍在Java中,对象、数组、字符串在内存是如何存储的,它们需要占用多少内存。

对象的内存

要想知道一个对象使用的内存量,需要将所有实例变量使用的内存与对象本身的开销(一般是16个字节)相加。这些开销包括一个指向对象的类的引用、垃圾收集信息以及同步信息。另外,一般内存的使用都会被填充为8个字节(64位计算机中的机器字)的倍数。如下图所示,对于一个Integer对象

int

一个Integer对象会使用24字节(16个字节的对象开销,4个字节用于保存它的int值以及4个填充字节)。

一个Date对象(如下图)需要使用32字节:16个字节的对象开销,3个int实例变量各需4个字节,以及4个填充字节。

date

对象的引用一般都是一个内存地址,因此会使用8个字节。

例如对于一个Counter对象

counter

它需要32字节:16个字节的对象开销,8个字节用于它的String型实例变量(一个引用),4字节用于int实例变量,以及4个填充字节。

当我们说明一个引用所占的内存时,我们会单独说它所指向的对象所占用的内存,因此,这个内存使用总量并没有包含String对象所使用的内存。

数组

在java中,数组一般被实现为对象,它们一般都会因为记录长度而需要额外的内存。

一个原始数据类型的数组一般需要24字节的头信息(如下)和保存值所需的引用。

  1. 16个字节的对象开销;
  2. 4个字节用于保存长度;
  3. 4个填充字节。

下面分别介绍一下int[]double[]、对象数组和二维数组的内存占用情况。

array

int值的数组

一个含有N个int值的数组需要使用$24+4N$字节(notice:最后会被填充为8的倍数)。

double值的数组

一个含有N个double值的数组需要使用$24+8N$字节(notice:最后会被填充为8的倍数)。

对象的数组

一个对象的数组就是一个对象的引用的数组,所以我们应该在对象所需的内存之外再加上引用所需的内存。例如,对于一个含有N个Date对象的数组需要使用24字节(头信息)加上$8N$字节(所有的引用)加上每个对象的32字节,总共$24+40N$。

二维数组

对于二维数组而言,它就是一个数组的数组(每个数组都是一个对象)。例如:一个$M*N$的double类型的二维数组需要使用24字节(头信息)加上$8M$字节(所有元素数组的引用)加上$24M$字节(所有元素的开销)加上$8MN$字节($M$个长度为$N$的double类型的数组),总和共$8MN+32M+24$字节。

String

一个String对象

string

String的标准实现含有4个实例变量

  • 一个指向字符数组的引用(8字节);
  • 三个int值(4个字节)
    1. 第一个int值:字符数组中的偏移量;
    2. 第二个int值:一个计数器,也就是字符串的长度,以上图为例,对象所表示的字符串由value[offset]value[offset+count-1]中的字符组成;
    3. 第三个int值:散列值。

因此,每个String对象都会使用40字节(16字节表示对象,三个int实例变量各需4个字节,加上数组引用的8个字节和4个填充字节)。这是除字符数组之外字符所需的内存空间,所有字符所需的内存需要另记,因为String的char数组常常是在多个字符串之间共享的。因为String对象是不可变的,这种设计使String的实现能够在多个对象都含有相同的value[]数组时节省内存。

字符串的值和子字符串

字符串和子字符串的例子如下所示:

substring

一个长度为N的String对象一般需要使用40字节(String对象本身)加上$24+2N$字节(字符数组),总共$64+2N$字节。但在处理字符串时经常会和子字符串打交道,所以Java对字符串的表示希望能够避免复制字符串中的字符。

当调用substring()方法时,就创建了一个新的String对象(40字节),但它仍然重用了相同的value[]数组,因此该字符串的子字符串只会使用40字节的内存。含有原始字符串的字符数组的别名存在于子字符串中,字符串对象的偏移量和长度域标记了子字符串的位置。

这些基础机制能够有效地帮助我们估计大量程序对内存的使用情况,但是很多复杂的因素仍然会使这个任务变得困难,这就得看一下JVM方面的书籍了。


参考: