前言:
Java最重要的特征之一就是它的内存分配系统,我们在分析Java对象的内存的时候,可以将它分解为基本的原始数据类型,而原始数据类型在Java中占用的大小是预先定义好的。只需要将变量的数量和他们预先定义好的字节数相乘即可。,下图是Java中原始数据类型的常见内存需求。
但是因为内存的使用和具体的机器有关,这里为了方便描述,统一采用64位机器,即表示机器地址需要8字节。
对象:
要直到一个对象使用的内存量需要将所有变量使用的内存与对象本身的开销(一般占用16字节)相加。这些开销包括指向对象的类的引用、垃圾搜集信息和同步信息。因为是在64位机器上,所以内存使用一般都为被填充为8字节的倍数,例如:一个Integer类型占用24字节,其中16字节对象开销,4字节int值以及4字节填充。
如图:
public class Integer
{ private int x;
……
}
16字节对象开销 |
4字节 int值 |
4字节填充 |
类似的,一个Date对象:
public class Date
{ private int day;
private int month;
private int year;
……
}
16字节对象开销 |
4字节 int值 day |
4字节 int值 month |
4字节 int值 year |
4字节填充 |
32字节
引用指向的对象所占用的内存我们需要单独说明,所以这个对象里并没有包含实际的string对象占用的内存。
链表:
我们以前面介绍的内部类Node作为例子,这里还需要额外的8字节指向外部类的引用。因此,一个Node类需要16个字节的对象开销,8字节指向外部类,8字节指向T类型对象的引用,8字节指向下一个Node对象的引用,总共40字节。当T类型为Integer类型时,因为Integer需要24字节,所以含有N个整数的链表需要(32+64N)字节,包括Stack对象的16字节开销,引用实例类型8字节,int4字节。如图:
相关代码:
public class Stack<T>
{
private Node first;
private int n;
private class Node<T>
{
private Node next;
private T t;
}
}
Stack:
16字节的对象开销 |
8字节Node的引用 |
4字节int |
Node:
16字节的对象开销 |
8字节指向Node的引用 |
8字节指向T类型的引用 |
8字节指向外部类的引用 |
Integer:
16字节对象开销 |
4字节 int值 |
4字节填充 |
数组:
Java中数组被实现为对象,他们需要额外的内存来记录长度。因此一般需要24字节的头信息,包括16字节的对象开销,4字节记录长度,4字节填充。例如一个含有N个double的数组需要(24+8N)字节,含有N个int的数组需要(24+4N)字节(会被填充为8字节的倍数),如果是一个对象数组,实际上数组里存的是对象的引用。例如一个含有N个Date对象的数组需要24字节+8N字节的引用+每个 对象占用32字节,总共(24+8N+32N)字节。
上面的例子如下图:
int[] ary=new int[N];
16字节的对象开销 |
4字节的长度记录 |
4字节填充 |
4字节int |
4字节int |
4字节int |
…… |
4字节int |
4字节int |
4字节int |
注:实际大小会被填充为8字节的倍数
double[] ary=new double[N];
16字节的对象开销 |
4字节的长度记录 |
4字节填充 |
8字节double |
8字节double |
8字节double |
…… |
8字节double |
8字节double |
8字节double |
Date[] ary=new Date[N];
16字节的对象开销 |
4字节的长度记录 |
4字节填充 |
8字节Date引用 |
8字节Date引用 |
8字节Date引用 |
…… |
8字节Date引用 |
8字节Date引用 |
8字节Date引用 |
第一个date
16字节对象开销 |
4字节 int值 day |
4字节 int值 month |
4字节 int值 year |
4字节填充 |
第二个date
16字节对象开销 |
4字节 int值 day |
4字节 int值 month |
4字节 int值 year |
4字节填充 |
......
第N个date
16字节对象开销 |
4字节 int值 day |
4字节 int值 month |
4字节 int值 year |
4字节填充 |
二维数组:
实际上是数组的数组。如图:
double[][] ary=new double[M][N]
总共M个数组引用:
16字节的对象开销 |
4字节的长度记录 |
4字节填充 |
8字节数组引用 |
8字节数组引用 |
8字节数组引用 |
…… |
8字节数组引用 |
8字节数组引用 |
8字节数组引用 |
16字节的对象开销 |
4字节的长度记录 |
4字节填充 |
8字节double |
8字节double |
8字节double |
…… |
8字节double |
8字节double |
8字节double |
总共N个Double
所以一共需要24(数组的数组的开销)+8M(所有元素数组的引用)+24M(所有数组元素的开销)+8MN(M个长度为N的double类型的数组)字节,对象的二维数组和这类似。
字符串:
Java中String的标准实现含有4个实例变量,一个指向字符数组的引用(8字节 ),剩下3个int值(4字节),一个描述字符数组中的偏移量,一个计数器来保存字符串的长度,最后一个保存该String对象的哈希值,在某些情况下可以省略计算。因此,一个一个String对象会使用16(对象开销)+4(int)+4(int )+4(int )+8(指向数组的引用)+4(填充)共40字节。这些是除了字符数组外字符长所需要的内存空间,所有字符需要的内存另计,因为String的char数组很多时候是共享的(String为不可变)。
16字节的对象开销 |
4字节int |
4字节int |
4字节int |
8字节指向数组引用 |
4字节填充 |
一个长度为N的String对象一般需要40字节加上24+2N字节(字符数组),但是在字符串中经常需要用到子串,所以Java对字符串的表示希望能避免复制字符串中的数组,例如,当调用String的substring()方法时,就创建了一个新的String对象(40字节),但是它仍然重用了相同的数组。因此该字符串的子串只会使用40字节,该子串保存一个原始字符数组的别名,并且它的偏移量和长度域标记了子字符串的位置。换句话说,一个子字符串需要的内存是一个常数,构造一个子串需要的时间也是一个常数。(这是1.6所采用的方法,但是因为会造成内存泄露,1.7后substring方法会在堆内存中创建一个新的数组。)