前言
典型的 Java 实现使用 8 位表示字节,用 2 字节(16 位)表示一个 char 值,
用 4 字节(32 位)表示一个 int 值,用 8 字节(64 位)表示一个 double 或者 long 值,用 1 字节表示一个 boolean 值(因为计算机访问内存的方式都是一次 1 字节)。根据可用内存的总量就能够计算出保存这些值的极限数量。例如,如果计算机有 1 GB 内存(10 亿字节),那么同一时间最多能在内存中保存 2.56 亿万个 int 值或是 1.28 亿万个 double 值。
许多数据结果都涉及对机器地址的表示,而在各种计算机中一个机器地址所需的内存又各有不同。为了保持一致,假设表示机器地址需要 8 字节(C语言中的指针就是这个大小),这是现在广泛使用的 64 位构架中的典型表示方式,许多老式的 32 位构架只使用 4 字节表示机器地址。
一个对象的内存占用情况
要知道一个对象所使用的内存量,需要将所有实例变量使用的内存与对象本身的开销(一般是16 字节)相加。这些开销包括一个指向对象的类的引用、垃圾收集信息以及同步信息。另外,一般内存的使用都会被填充为8字节(64位计算机中的机器字)
的倍数。例如,一个 Integer 对象会使用 24 字节(16字节的对象开销,4 字节用于保存它的 int 值以及 4个填充字节)。类似地,一个Date对象需要使用 32 字节:16 字节的对象开销,3 个 int 实例变量各需 4 字节,以及 4 个填充字节。对象的引用
一般都是一个内存地址,因此会使用 8 字节。例如,一个 Counter 对象需要使用 32 字节:16 字节的对象开销,8 字节用于它的 String 型实例变量(一个引用),4 字节用于 int 实例变量,以及4 个填充字节。当我们说明一个引用所占的内存时,我们会单独说明它所指向的对象所占用的内存,因此这个内存使用总量并没有包含 String 值所使用的内存。
链表
嵌套的非静态(内部)类,例如我们的Node类,还需要额外的 8 字节(用于一个指
向外部类的引用)。因此,一个 Node 对象需要使用 40 字节(16 字节的对象开销,指向 Item 和 Node对象的引用各需 8 字节,另外还有 8 字节的额外开销)。因为 Integer 对象需要使用 24 字节,一个含
有 N 个整数的基于链表的栈(请见算法 1.2)需要使
用(32+64N)字节,包括 Stack 对象的 16 字节的开
销,引用类型实例变量8字节,int型实例变量4字节,
4 个填充字节,每个元素需要 64 字节,一个 Node 对象的 40 字节和一个 Integer 对象的 24 字节。
数组
Java 中数组被实现为对象,它们一般都会因为记录长度而需要额外的内存。一个原始数据类型的数组一般需要 24 字节的头信息(16字节的对象开销,4 字节用于保存长度以及 4 填充字节)再加上保存值所需的内存。例如,一个含有 N 个 int 值的数组需要使用(24 + 4N)字节(会被填充为 8 的倍数),一个含有 N 个 double值的数组需要使用(24 + 8N)字节。一个对象的数组就是一个对象的引用的数组,所以我们应该在对象所需的内存之外加上引用所需的内存。例如,一个含有 N 个 Date 对象的数
组需要使用 24 字节(数组开销)加上 8N 字节(所有引用)加上每个对象的 32 字节,总共(24 + 40N)字节。二维数组是一个数组的数组(每个数组都是一个对象)。例如,一个 M×N 的 double类型的二维数组需要使用 24 字节(数组的数组的开销)加上 8M 字节(所有元素数组的引用)加上 24M 字节(所有元素数组的开销)加上 8MN 字节(M 个长度为 N 的 double 类型的数组),总共(8MN+32M+24)~ 8MN 字节;当数组元素是对象时计算方法类似,结果相同,用来保存充满指向数组对象的引用的数组以及所有这些对象本身。
字符串对象
我们可以用相同的方式说明 Java 的 String 类型对象所需的内存,只是对于字符串来说别名是非常常见的。String 的标准实现含有 4 个实例变量:一个指向字符数组的引用(8 字节)和三个 int 值(各 4 字节)。第一个 int 值描述的是字符数组中的偏移量,第二个 int 值是一个计数器(字符串的长度)。按照下图所示的实例变量名,对象所表示的字符串由 value[offset]到 value[offset + count - 1] 中的字符组成。String 对象中的第三个 int 值是一个散列值,它在某些情况下可以节省一些计算,现在可以忽略它。因此,每个 String 对象总共会使用字节(16 字节表示对象,三个 int 实例变量各需 4 字节,加上数组引用的 8 字节和 4 个填充字节)。这是除字符数组之外字符串所需的内存空间,所有字符所需的内存需要另记,因为 String 的 char数组常常是在多个字符串之间共享的。因为 String 对象是不可变的,这种设计使 String 的实现在能够在多个对象都含有相同的 value[] 数组时节省内存。
字符串的值和子字符串
一个长度为 N 的 String 对象一般需要使用 40 字节(String 对象本身)加(24+2N)字节(字符数组),总共(64+2N)字节。但字符串处理经常会和子字符串打交道,所以 Java 对字符串的表示希望能够避免复制字符串中的字符。当你调用 substring() 方法时,就创建了一个新的 String对象(40 字节),但它仍然重用了相同的 value[] 数组,因此该字符串的子字符串只会使用 40 字节的内存。含有原始字符串的字符数组的别名存在于子字符串中,子字符串对象的偏移量和长度域标记了子字符串的位置。换句话说,一个子字符串所需的额外内存是一个常数,构造一个子字符串所需的时间也是常数,即使字符串和子字符串的长度极大也是这样。某些简陋的字符串表示方法在创建子字符串时需要复制其中的字符,这将需要线性的时间和空间。确保子字符串的创建所需的空间(以及时间)和其长度无关是许多基础字符串处理算法的效率的关键所在。字符串的值与子字符串示例如上图所示。这些基础机制能够有效帮助我们估计大量程序对内存的使用情况,但许多复杂的因素仍然会使这个任务变得更加困难。我们已经提到了别名可能产生的潜在影响。另外,当涉及函数调用时,内存的消耗就变成了一个复杂的动态过程,因为 Java 系统的内存分配机制扮演一个重要的角色,而这套机制又和 Java 的实现有关。例如,当你的程序调用一个方法时,系统会从内存中的一个特定区域为方法分配所需要的内存(用于保存局部变量),这个区域叫做栈(Java 系统的下压栈)。当方法返回时,它所占用的内存也被返回给了系统栈。因此,在递归程序中创建数组或是其他大型对象是很危险的,因为这意味着每一次递归调用都会使用大量的内存。当通过 new 创建对象时,系统会从堆内存的另一块特定区域为该对象分配所需的内存。而且,你要记住所有对象都会一直存在,直到对它的引用消失为止。此时系统的垃圾回收进程会将它所占用的内存收回到堆中。这种动态过程使准确估计一个程序的内存使用变得极为困难。