面试官欺负人:new Object()到底占用几个字节?
时间:2021-10-29 13:55:45
手机看文章
扫描二维码
随时随地手机看文章
[导读]前言我们来分析一下堆内布局以及Java对象在内存中的布局吧。对象的指向先来看一段代码:package com.zwx.jvm;public class HeapMemory { private Object obj1 = new Object(); public st...
前言
我们来分析一下堆内布局以及Java对象在内存中的布局吧。
对象的指向
先来看一段代码:
public class HeapMemory {
private Object obj1 = new Object();
public static void main(String[] args) {
Object obj2 = new Object();
}
}
123456789 上面的代码中,obj1 和obj2在内存中有什么区别?
Java内存模型
对象内存中可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding),以64位操作系统为例(未开启指针压缩的情况)Java对象布局如下图所示:其中对象头中的Mark Word中的详细信息在文章synchronized锁升级原理中有详细介绍。上图中的对齐填充不是一定有的,如果对象头和实例数据加起来刚好是8字节的倍数,那么就不需要对齐填充。
Object obj=new Object()占用字节
这是网上很多人都会提到的一个问题,那么结合上面的Java内存布局,我们来分析下,以64位操作系统为例,new Object()占用大小分为两种情况:
- 未开启指针压缩 占用大小为:8(Mark Word) 8(Class Pointer)=16字节
- 开启了指针压缩(默认是开启的) 开启指针压缩后,Class Pointer会被压缩为4字节,最终大小为:8(Mark Word) 4(Class Pointer) 4(对齐填充)=16字节
org.openjdk.jol
jol-core
0.10
12345 然后新建一个简单的demo:
import org.openjdk.jol.info.ClassLayout;
public class HeapMemory {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
12345678910 输出结果如下:最后的结果是16字节,没有问题,这是因为默认开启了指针压缩,那我们现在把指针压缩关闭之后再去试试。
-XX:-UseCompressedOops 关闭指针压缩
12 再次运行,得到如下结果:可以看到,这时候已经没有了对齐填充部分了,但是占用大小还是16位。
public class MyItem {
byte i = 0;
}
12345 然后分别在开启指针压缩和关闭指针压缩的场景下分别输出这个类的大小。
import org.openjdk.jol.info.ClassLayout;
public class HeapMemory {
public static void main(String[] args) {
MyItem myItem = new MyItem();
System.out.println(ClassLayout.parseInstance(myItem).toPrintable());
}
}
12345678910 开启指针压缩,占用16字节:关闭指针压缩,占用24字节:这个时候就能看出来开启了指针压缩的优势了,如果不断创建大量对象,指针压缩对性能还是有一定优化的。
对象的访问
创建好一个对象之后,当然需要去访问它,那么当我们需要访问一个对象的时候,是如何定位到对象的呢?目前最主流的访问对象方式有两种:句柄访问和直接指针访问。
- 句柄访问 使用句柄访问的话,Java虚拟机会在堆内划分出一块内存来存储句柄池,那么对象当中存储的就是句柄地址,然后句柄池中才会存储对象实例数据和对象类型数据地址。
- 直接指针访问(Hot Spot虚拟机采用的方式) 直接指针访问的话对象中就会直接存储对象类型数据。
句柄访问和直接指针访问对比
上面图形中我们很容易对比,就是如果使用句柄访问的时候,会多了一次指针定位,但是他也有一个好处就是,假如一个对象被移动(地址改变了),那么只需要改变句柄池的指向就可以了,不需要修改reference对象内的指向,而如果使用直接指针访问,就还需要到局部变量表内修改reference指向。
堆内存
上面我们提到,在Java对象头当中的Mark Word存储了对象的分代年龄,那么什么是分代年龄呢?
Young区
现在拆分成了Young区,那我们看下面一个场景,下面的Young是经过垃圾回收之后的一个概图:假如说现在来了一个对象,要占用2个对象的大小,会发现放不下去了,这时候就会触发GC(垃圾回收),但是一旦触发了GC(垃圾回收),对用户线程是有影响的,因为GC过程中为了确保对象引用不会不断变化,需要停止所有用户线程,Sun把这个事件称之为:Stop the World(STW)。这些在下一篇讲解垃圾回收的时候会详细介绍,这里先不深入。
Old区
当Young区的对象达到设置的分代年龄之后,对象会进入Old区,Old区满了之后会触发Full GC,如果还是清理不掉空间,那么就抛出OutOfMemeoyError异常。
名词扫盲
上面提到了很多新的名词,而实际上很多这种名词还有其他叫法,这个还是觉得有必要了解一下。
- 垃圾回收:简称GC。
- Minor GC:针对新生代的GC
- Major GC:针对老年代的GC,一般老年代触发GC的同时也会触发Minor GC,也就等于触发了Full GC。
- Full GC:新生代 老年代同时发生GC。
- Young区:新生代
- Old区:老年代
- Eden区:暂时没发现有什么中文翻译(伊甸园?)
- Surcivor区:幸存区
- S0和S1:也称之为from区和to区,注意from和to两个区是不断互换身份的,且S0和S1一定要相等,并且保证一块区域是空的
一个对象的人生轨迹图
从上面的介绍大家应该有一个大致的印象,一个对象会在Eden区,S0区,S1区,Old区不断流转(当然,一开始就会被回收的短命对象除外),我们可以得到下面的一个流程图:
总结
本文主要介绍了一个Java对象在堆内是如何存储的,并结合Java对象的内存布局示范了一个普通对象占用大小问题,然后还分析了堆内的空间划分以及划分原因,本文中涉及到了GC相关知识均没有深入讲解,关于GC及GC算法和GC收集器等相关知识将放在下一篇进行详细分析。