对象内存问题
对象的内存布局
对象的内存布局是由 Java 虚拟机规范定义的,但具体的实现细节各有不
同,如 HotSpot 和 OpenJ9 就不一样。
就拿我们常用的 HotSpot 来说吧。
对象在内存中包括三部分:对象头、实例数据和对齐填充。
对象头
对象头是对象存储在内存中的元信息,包含了Mark Word、类型指针等信
息。
Mark Word 存储了对象的运行时状态信息,包括锁、哈希值、GC 标记
等。在 64 位操作系统下占 8 个字节,32 位操作系统下占 4 个字节。
类型指针指向对象所属类的元数据,也就是 Class 对象,用来支持多态、
方法调用等功能。
除此之外,如果对象是数组类型,还会有一个额外的数组长度字段。占 4
个字节。
类型指针会被压缩吗?
类型指针可能会被压缩,以节省内存空间。比如说在开启压缩指针的情况下
占 4 个字节,否则占 8 个字节。在 JDK 8 中,压缩指针默认是开启的。
实例数据
实例数据是对象实际的字段值,也就是成员变量的值,按照字段在类中声
明的顺序存储。JVM 会对这些数据进行对齐/重排,以提高内存访问速度。
对齐填充
由于 JVM 的内存模型要求对象的起始地址是 8 字节对齐(64 位 JVM
中),因此对象的总大小必须是 8 字节的倍数。
如果对象头和实例数据的总长度不是 8 的倍数,JVM 会通过填充额外的
字节来对齐。
CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存
行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓
存行加载,CPU 的访问效率就会降低。
提问: new Object() 对象的内存大小?
一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默
认开启的,因此在 64 位的 JVM 上,new Object()的大小是 16 字节(12
字节的对象头 + 4 字节的对齐填充)。
对象头的大小是固定的,在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是
16 字节;如果开启了压缩指针,就是 12 字节。
实例数据的大小取决于对象的成员变量和它们的类型。对于new
Object()来说,由于默认没有成员变量,因此我们可以认为此时的实例数
据大小是 0。
文章推荐阅读:Object o = new Object()占多少个字节?
JVM访问对象的方法?
主流的方式有两种:句柄和直接指针。HotSpot 虚拟机主要使用直接指针来进行对象访问。
句柄是通过一个中间的句柄表来定位对象的,而直接指针则是通过引用直接指向对象的内存地址。优点是,对象被移动时只需要修改句柄表中的指针,而不需要修改对象引用本身。
在直接指针访问中,引用直接存储对象的内存地址;对象的实例数据和类型信息都存储在堆中固定的内存区域。优点是访问速度更快,因为少了一次句柄的寻址操作。缺点是如果对象在内存中移动,引用需要更新为新的地址。
对象的四种引用?
强引用
强引用是 Java 中最常见的引用类型。使用 new 关键字赋值的引用就是强引用,只要强引用关联着对象,垃圾收集器就不会回收这部分对象,即使内存不足。软引用
软引用于描述一些非必须对象,通过 SoftReference 类实现。软引用的对
象在内存不足时会被回收。
- 弱引用
弱引用用于描述一些短生命周期的非必须对象,如ThreadLocal中的Entry,就是通WeakReference 类实现的。弱引用的对象会在下一次垃圾回收时会被回收,不论内存是否充足。
- 虚引用
虚引用主要用来跟踪对象被垃圾回收的过程,通过 PhantomReference 类实
现。虚引用的对象在任何时候都可能被回收。
对象一定分配在堆中吗?
不一定。
默认情况下,Java 对象是在堆中分配的,但 JVM 会进行逃逸分析,来判断对象的生命周期是否只在方法内部,如果是的话,这个对象可以在栈上分配。
什么是逃逸分析?
逃逸分析是一种 JVM 优化技术,用来分析对象的作用域和生命周期,判断对象是否逃逸出方法或线程。
可以通过分析对象的引用流向,判断对象是否被方法返回、赋值到全局变量、传递到其他线程等,来确定对象是否逃逸。如果对象没有逃逸,就可以进行栈上分配、同步消除、标量替换等优化,以提高程序的性能。
逃逸具体是指什么?
根据对象逃逸的范围,可以分为方法逃逸和线程逃逸。
当对象被方法外部的代码引用,生命周期超出了方法的范围,那么对象就必须分配在堆中,由垃圾收集器管理。
逃逸分析会带来什么好处?
主要有三个。
第一,如果确定一个对象不会逃逸,那么就可以考虑栈上分配,对象占用的内存随着栈帧出栈后销毁,这样一来,垃圾收集的压力就降低很多。
第二,线程同步需要加锁,加锁就要占用系统资源,如果逃逸分析能够确定一个对象不会逃逸出线程,那么这个对象就不用加锁,从而减少线程同步
的开销。
第三,如果对象的字段在方法中独立使用,JVM
可以将对象分解为标量变量,避免对象分配。