【从浅到深的算法技巧】内存

3.内存

一个程序对内存的使用也和物理世界直接相关:计算机中的电路很大一部分的作用就是帮助程序保存一些值并在稍后取出它们。在任意时刻需要保存的值越多,需要的电路也就越多。你可能知道计算机能够使用的内存上限(知道这一·点的人应该比知道运行时间限制的人要多)因为你很可能已经在内存上花了不少额外的支出。

计算机上的Java对内存的使用经过了精心的设计(程序的每个值在每次运行时所需的内存量都是一样的),但实现了Java的设备非常多,面内存的使用是和实现相关的。简单起见,我们用典型这个词暗示和机器相关的值。

Java最重要的特性之一就是它的内存分配系统。它的任务是把你从对内存的操作之中解脱出来。分析内存的使用比分析程序所需的运行时间要简单得多,主要原因是它所涉及的程序语句较少(只有声明语句)且在分析中我们会将复杂的对象简化为原始数据类型,而原始数据类型的内存使用是预先定义好的,而且非常容易理解:只需将变量的数量和它们的类型所对应的字节数分别相乘并汇总即可。例如,因为Java的int数据类型是-2147483648到2147483647之间的整数值的集合,典型的Java实现使用32位来表示int值。其他原始数据类型的内存使用也是基于类似的考虑:典型的Java实现使用8位表示字节,用2字节(16位)表示一个 char值,用4字节(32位)表示一个int值,用8字节(64位)表示一个double或者long值,用1字节表示一个boolean值(因为计算机访向内存的方式都是一次1字节)。根据可用内存的总最就能够计算出保存这些值的极限数量。例如,如果计算机有1 GB内存(10亿字节),那么同一时间最多能在内存中保存3200万个int值或是1600万个double值。

3.1 对象

要知道一个对象所使用的内存量,需要将所有实例变量使用的内存与对象本身的开销(一般是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值所使用的内存。

3.2链表

嵌套的非静态(内部)类.例如我们的Node类(请见1.3.3.1 节),还需要额外的8字节(用于一个指向外部类的引用)。因此,一个Node对象需要使用40字节(16字节的对象开销,指向Item和Node对象的引用各需8字节,另外还有8字节的额外开销)。因为Integer对象需要使用24字节,一个含有N个整数的基于链表的栈需要使用(32+64N) 字节,包括Stack对象的16字节的开销,引用类型实例变量8字节。int型实例变量4字节,4个填充字节,每个元素需要64字节,一个Node对象的40字节和一个Integer对象的24字节。

3.3 数组

Java 中数组被实现为对象,它们一般都会因为记录长度而需要额外的内存。一个原始数据类型的数组一般需要24字节的头信息( 16字节的对象开销,4字节用于保存长度以及4填充字节)再加上保存值所需的内存。例如,一个含有N个int值的数组需要使用(24+4N)字节(会被填充为8的倍数),一个含有N个double值的数组需要使用(24 +8N)字节。一个对象的数组就是一个对象的引用的数组,所以我们应该在对象所需的内存之外加上引用所需的内存。例如,一个含有N个Date对象(请见表1.2.12) 的数组需要使用24字节(数组开销)加上8N字节(所有引用)加上每个对象的32字节,总共(24+40N)字节。二维数组是一个数组的数组(每个数组都是一个对象)。例如,一个MXN的double类型的-维数组需要使用24字节(数组的数组的开销)加上8M字节(所有元素数组的引用)加上24M字节(所有元素数组的开销)加上8MN字节(M个长度为N的double类型的数组),总共(8MN+32M+24) ~ 8MN字节;当数组元素是对象时计算方法类似,结果相同,用来保存充满指向数组对象的引用的数组以及所有这些对象本身。

3.4 字符串对象

我们可以用相同的方式说明Java的String类型对象所需的内存,只是对于字符串来说别名是非常常见的。String 的标准实现含有4个实例变量:个指向字符数组的引用(8字节)和三个int值(各4字节)。所有字符所需的内存需要另记,因为string的char数组常常是在多个字符串之间共享的。因为String对象是不可变的,这种设计使String的实现在能够在多个对象都含有相同的value[]数组时节省内存。

3.5 字符串的值和子字符串

一个长度为N的String对象一般需要使用40字节(String对象本身)加上(24+2N)字节(字符数组),总共(64+2N)字节。但字符串处理经常会和子字符串打交道,所以Java对字符串的表示希望能够避免复制字符串中的字符。当你调用substring()方法时,就创建了一个新的String对象(40字节),但它仍然重用了相同的value[]数组,因此该字符串的子字符串只会使用40字节的内存。含有原始字符串的字符数组的别名存在于子字符串中,子字符串对象的偏移量和长度城标记了子字符串的位置。换句话说,一个子字符串所需的额外内存是一个常数,构造一个子字符串所需的时间也是常数,即使字符串和子字符串的长度极大也是这样。某些简陋的字符串表示方法在创建子字符串时需要复制其中的字符,这将需要线String对象 (Java库)性的时间和空间。确保子字符串的创建所需的空间(以及时间)和其长度无关是许多基础字符串处理算法的效率的关键所在。

这些基础机制能够有效帮助我们估计大量程序对内存的使用情况,但许多复杂的因素仍然会使这个任务变得更加困难。我们已经提到了别名可能产生的潜在影响。另外,当涉及函数调用时,内存的消耗就变成了一个复杂的动态过程,因为Java系统的内存分配机制扮演一个重要的角色,而这套机制又和Java的实现有关。例如,当你的程序调用-方法时,系统会从内存中的一个特定区域为方法分配所需要的内存(用于保存局部变量),这个区城叫做栈(Java系统的下压栈)。当方法返回时,它所占用的内存也被返回给了系统栈。因此,在递归程序中创建数组或是其他大型对象是很危险的,因为这意味着每一次递归调用都会使用大量的内存。当通过new创建对象时,系统会从堆内存的另一块特定区域为该对象分配所需的内存。而且,所有对象都会一直存在, 直到对它的引用消失为止。此时系统的垃圾回收进程会将它所占用的内存收回到堆中。这种动态过程使准确估计一个程序的内存使用变得极为困难。

你可能感兴趣的:(从浅到深的算法技巧,算法,内存,java)