一个Java对象和Hashmap对象占用多大内存

一个Java对象占用多少字节?

最近在读《深入理解Java虚拟机》,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存?

想弄清楚上面的问题,先补充一下基础知识。

1、JAVA 对象布局

在 HotSpot虚拟机中,对象在内存中的存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)

某些情况下,JVM根本就没有把Object放入堆中。例如:原则上讲,一个小的thread-local对象存在于栈中,而不是在堆中。
被Object占用内存的大小依赖于Object的当前状态。例如:Object的同步锁是否生效,或者,Object是否正在被回收。
我们先来看看在堆中单个的Object长什么样子
在这里插入图片描述
在堆中,每个对象由四个域构成(A、B、C 和 D),下面我们逐个解释一下:
A:对象头,占用很少的字节,表述Object当前状态的信息
B:基本类型域占用的空间(原生域指 int、boolean、short等)
C:引用类型域占用的空间(引用类型域指 其他对象的引用,每个引用占用4个字节)
D:填充物占用的空间(后面说明什么是填充物)

1.1对象头(Header):

Java中对象头由 Markword(8byte) + 类指针kclass(该指针指向该类型在方法区的元类型,默认开启压缩4byte,不开启为8byte) 组成,所以默认为12byte。

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。在64位的虚拟机(未开启压缩指针)为64bit。
Markword:
在这里插入图片描述
类指针kclass:
kclass存储的是该对象所属的类在方法区的地址,所以是一个指针,默认Jvm对指针进行了压缩,用4个字节存储,如果不压缩就是8个字节。 关于Compressed Oops的知识,大家可以自行查阅相关资料来加深理解。

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,这块占用4个字节(即数组默认为8+4+4=16byte)。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)

1.2实例数据(Instance Data)

实例数据部分就是成员变量的值,其中包含父类的成员变量和本类的成员变量。也就是说,除去静态变量和常量值放在方法区,非静态变量的值是随着对象存储在堆中的。
因为修改静态变量会反映到方法区中class的数据结构中,故而推测对象保存的是静态变量和常量的引用。

1.3对齐填充(Padding)

用于确保对象的总长度为8字节的整数倍。
HotSpot要求对象的总长度必须是8字节的整数倍。由于对象头一定是8字节的整数倍,但实例数据部分的长度是任意的。因此需要对齐补充字段确保整个对象的总长度为8的整数倍。

2、Java数据类型有哪些

  • 基础数据类型(primitive type)
  • 引用类型 (reference type)

2.1基础数据类型内存占用如下

在这里插入图片描述

2.2引用类型 内存占用如下:

引用类型跟基础数据类型不一样,除了对象本身之外,还存在一个指向它的引用(指针),指针占用的内存在64位虚拟机上8个字节,如果开启指针压缩是4个字节,默认是开启了的。

2.3字段重排序

为了更高效的使用内存,实例数据字段将会重排序。排序的优先级为: long = double > int = float > char = short > byte > boolean > object reference
如下所示的类

class FieldTest{
        byte a;
        int c;
        boolean d;
        long e;
        Object f;
    }

将会重排序为(开启CompressedOops选项):

   OFFSET  SIZE               TYPE DESCRIPTION            
         16     8               long FieldTest.e            
         24     4                int FieldTest.c            
         28     1               byte FieldTest.a            
         29     1            boolean FieldTest.d            
         30     2              (alignment/padding gap)
         32     8   java.lang.Object FieldTest.f

3、验证

讲完了上面的概念,我们可以去验证一下。
3.1有一个Fruit类继承了Object类,我们分别新建一个object和fruit,那他们分别占用多大的内存呢?

class Fruit extends Object {
     private int size;
}

Object object = new Object();
Fruit f = new Fruit();

先来看object对象,通过上面的知识,它的Markword是8个字节,kclass是4个字节, 加起来是12个字节,加上4个字节的对齐填充,所以它占用的空间是16个字节。

再来看fruit对象,同样的,它的Markword是8个字节,kclass是4个字节,但是它还有个size成员变量,int类型占4个字节,加起来刚好是16个字节,所以不需要对齐填充。

那该如何验证我们的结论呢?毕竟我们还是相信眼见为实!很幸运Jdk提供了一个工具jol-core可以让我们来分析对象头占用内存信息。具体使用参考
jol的使用也很简单:
打印头信息

public static void main(String[] args) {
	System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
	System.out.print(ClassLayout.parseClass(Object.class).toPrintable());
}

输出结果

com.zzx.algorithm.tst.Fruit object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int Fruit.size                                N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到输出结果都是16 bytes,跟我们前面的分析结果一致。

3.2 除了类类型和接口类型的对象,Java中还有数组类型的对象,数组类型的对象除了上面表述的字段外,还有4个字节存储数组的长度(所以数组的最大长度是Integer.MAX)。所以一个数组对象占用的内存是 8 + 4 + 4 = 16个字节,当然这里不包括数组内成员的内存。

我们也运行验证一下:

public static void main(String[] args) {
	String[] strArray = new String[0];
	System.out.println(ClassLayout.parseClass(strArray.getClass()).toPrintable());
}

输出结果:

[Ljava.lang.String; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0    16                    (object header)                           N/A
     16     0   java.lang.String String;.<elements>                        N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

输出结果object header的长度也是16,跟我们分析的一致。
3.3 接下来看对象的实例数据部分:
为了方便说明,我们新建一个Apple类继承上面的Fruit类

public class Apple extends Fruit {
	private int size;
	private String name;
	private Apple brother;
	private long create_time;
	
}

// 打印Apple的对象分布信息
System.out.println(ClassLayout.parseClass(Apple.class).toPrintable());

// 输出结果

com.zzx.algorithm.tst.Apple object internals:
 OFFSET  SIZE                          TYPE DESCRIPTION                               VALUE
      0    12                               (object header)                           N/A
     12     4                           int Fruit.size                                N/A
     16     8                          long Apple.create_time                         N/A
     24     4                           int Apple.size                                N/A
     28     4              java.lang.String Apple.name                                N/A
     32     4   com.zzx.algorithm.tst.Apple Apple.brother                             N/A
     36     4                               (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以看到Apple的对象头12个字节,然后分别是从Fruit类继承来的size属性(虽然Fruit的size是private的,还是会被继承,与Apple自身的size共存),还有自己定义的4个属性,基础数据类型直接分配,对象类型都是存的指针占4个字节(默认都是开启了指针压缩),最终是40个字节,所以我们new一个Apple对象,直接就会占用堆栈中40个字节的内存,清楚对象的内存分配,让我们在写代码时心中有数,应当时刻有内存优化的意识!
特别注意: String作为一个类的成员变量,是一个引用,大小为4byte , 但是如果单独拿出来计算String s=“string”;大小为24byte

这里又引出了一个小知识点,上面其实已经标注出来了。

父类的私有成员变量是否会被子类继承?
答案当然是肯定的,我们上面分析的Apple类,父类Fruit有一个private类型的size成员变量,Apple自身也有一个size成员变量,它们能够共存。注意划重点了,类的成员变量的私有访问控制符private,只是编译器层面的限制,在实际内存中不论是私有的,还是公开的,都按规则存放在一起,对虚拟机来说并没有什么分别!

4、方法内部new的对象是在堆上还是栈上?

我们常规的认识是对象的分配是在堆上,栈上会有个引用指向该对象(即存储它的地址),到底是不是呢,我们来做个试验!
我们在循环内创建一亿个Apple对象,并记录循环的执行时间,前面已经算过1个Apple对象占用40个字节,总共需要4GB的空间。

public static void main(String[] args) {
     long startTime = System.currentTimeMillis();
     for (int i = 0; i < 100000000; i++) {
         newApple();
     }
     System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms");
}

public static void newApple() {
     new Apple();
}

我们给JVM添加上-XX:+PrintGC运行配置,让编译器执行过程中输出GC的log日志
// 运行结果,没有输出任何gc的日志
take time:6ms

1亿个对象,6ms就分配完成,而且没有任何GC,显然如果对象在堆上分配的话是不可能的,其实上面的实例代码,Apple对象全部都是在栈上分配的,这里要提出一个概念指针逃逸,newApple方法中新建的对象Apple并没有在外部被使用,所以它被优化为在栈上分配,我们知道方法执行完成后该栈帧就会被清空,所以也就不会有GC。
我们可以设置虚拟机的运行参数来测试一下。
// 虚拟机关闭指针逃逸分析
-XX:-DoEscapeAnalysis

// 虚拟机关闭标量替换
-XX:-EliminateAllocations

在VM options里面添加上面二个参数,再运行一次

[GC (Allocation Failure)  236984K->440K(459776K), 0.0003751 secs]
[GC (Allocation Failure)  284600K->440K(516608K), 0.0004272 secs]
[GC (Allocation Failure)  341432K->440K(585216K), 0.0004835 secs]
[GC (Allocation Failure)  410040K->440K(667136K), 0.0004655 secs]
[GC (Allocation Failure)  491960K->440K(645632K), 0.0003837 secs]
[GC (Allocation Failure)  470456K->440K(625152K), 0.0003598 secs]

take time:5347ms

可以看到有很多GC的日志,而且运行的时间也比之前长了很多,因为这时候Apple对象的分配在堆上,而堆是所有线程共享的,所以分配的时候肯定有同步机制,而且触发了大量的gc,所以效率低很多。
总结一下: 虚拟机指针逃逸分析是默认开启的,对象不会逃逸的时候优先在栈上分配,否则在堆上分配。
到这里,关于“一个对象占多少内存?”这个问题,已经能回答的相当全面了。

参考文章:
https://blog.csdn.net/zzx410527/article/details/93646925

一个HashMap对象占多少字节?

对象=对象头+成员变量+对齐填充

对象头结构:java对象在Heap里面的结构是这样的:对象头跟对象体,对象体跟C里面的结构体是一样的,对象头由两个域组成:用于存放hashcode、同步、GC的_mask域,和指向方法区该对象Class对象的指针——_klass域,对于64位系统,头部长度理论上讲应该是8+8=16字节。但是从java6u23以后开始,64位的机器会自动开启指针压缩的功能,此时引用指针的长度为4字节。所以,对象头长度应该为8+4=12。

成员变量:分两类,包括一些基本类型,如int,long.byte,short,boolean等,以及引用类型,如String,Date引用。如果是引用类型,也应该把引用类型指向的对象纳入当前对象。

对齐填充:JVM规定,对象的大小必须是8字节的整数倍,如果不足,则会补齐。

此外,对于数组,还会有一个标示数组长度的字段。其实数组也是一种类,会在后文中介绍。

以此为理论基础,我们来计算一下常用的对象占用空间大小。

Integer

类结构图:可以看到,只有一个私有的int型数据

img

所以Integer长度为:头(8+4)+ int(4) = 16字节

Long

类结构图

img

类似于Integer,只有一个long型的私有成员。

所以总长度为:头(8+4)+long(8)+padding(4)=24字节

Object

类结构图

img

没有成员变量,所以占用空间头(8+4)+padding(4)=16字节

String:“string”

类结构图

img

这个结构稍微有点复杂,涉及到了数组成员。其实数组也是一种类型,只不过这种类型是JVM在运行时生成的类型,并不在class文件中定义,我们将其当做一种特殊的类就可以了。既然涉及到了成员变量是对象,那么,我们就要把String分成两部分来计算:

String类型:头部(8+4)+int(4)+int(4)+指向char[]对象的引用类型(4)=24字节

char[]类型:数组类型比普通对象多一个标示数组长度的字段,占4个字节。对于字符串“String”来说,头部(8+4)+数组长度(4)+“String”(2*6)+padding(4)=32字节

因此,它的总占用空间为56字节

ArrayList

类结构图

img

img

其实,还有一个 modCount成员,继承自AbstractList类,那么对于一个 list = new ArrayList(); list.add(“String”);的list来说,它拥有两个int,一个大小为10的数组(当 list.add() 第一个元素的时候,它会初始化elementData为一个长度10的数组)

ArrayList: 头部(8+4)+int(4)+int(4)+数组引用(4)=24字节

elementData[] : 头部(8+4)+长度(4)+string引用(4*10)=56字节

"String"字符串:这个我们之前计算过了,为56字节

所以,总空间大小为24+56+56=136字节

HashMap

类结构图

img

HashMap内部结构比较复杂,除了一些基本的类型,还有比较复杂一点的集合类型。如table,是一个Entry数组,用来存放键值对,所有put进map中key-value都会被封装成一个entry放入到table中去。而还有一些辅助对象,如entry,继承自AbstractMap的keySet,values,这些都是在遍历map元素时用到的集合,他们的主要功能是通过在自己内部维护一个迭代器向外输出table中的数据,并不实际储存key-value数据。

以 Map<String,String> map = new HashMap<String,String>(); 这时候我们计算一下他的占用空间情况:

img

总空间为:48+16=64字节

hashmap:头部(8)+int(4*4)+float(4)+table数组引用(4)+entrySet引用(4)+keySet引用(4)+values引用(4)+padding(4)=48字节

table:头部(8+4)+长度(4)=16字节

然后我们put进去一条数据:map.put( “100002”, “张明”);

当HashMap初始化的时候,他会开辟一个长度为16的table数组,每当put一个新的key-value的时候,他会根据当前threshold来判断是否需要扩容,如果需要扩容,则会以倍数增长的方式扩容table数组。如16、32、64.具体原理请参考 http://blog.csdn.net/zq602316498/article/details/39351363

接下来让我们计算一下这个map多占用的空间

img

hashmap:头部(8)+int(4*4)+float(4)+table数组引用(4)+entrySet引用(4)+keySet引用(4)+values引用(4)+padding(4)=48字节

table: 80+32+16+16+56+48+0= 216字节

table:头部(8+4)+长度(4)+entry(4*16)=80字节

entry:头部(8+4)+k(4)+value(4)+next(4)+int(4)+padding(4)=32字节

img

key(String):56字节

value(String) :48字节

next :因为就只有一个元素,所以next值为null,0字节

entrySet:为空指针,0字节

keySet:空指针,0字节

values:空指针,0字节

综上分析,这个map占用48+216+0+0+0=264字节

然后我们继续调用 map.keySet() 方法,此时,keySet会被赋予一个类型为 HashMap$KeySet 的对象,这个对象的结构如下:

img

可以看到,它并不复杂,只是用来遍历map key集合的一个工具类,

keySet : 头部(8+4)+padding(4)=16字节

所以,总大小为264+16=280字节

然后我们继续调动 map.values(),和上面类似

img

values : 头部(8+4)+padding(4)=16字节

所以,总大小为 280+16=296字节

然后我们继续调用 map.entrySet(),

img

entrySet:头部(8+4)+padding(4)=16字节

所以总大小为 296+16=312字节

参考文章:
https://cloud.tencent.com/developer/article/1441801

发布了107 篇原创文章 · 获赞 14 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/belongtocode/article/details/103377187