虚拟机对象
# 1 对象的创建包含哪些步骤?
# 1.1 类加载检查
虚拟机遇到一条new指令时,首先去检查这个指令的参数能否在Class常量池中定位到一个类的符号引用,且检查这个符号引用代表的类是否已加载、解析和初始化过。如果没有,先执行相应的类加载过程。
new指令指:new关键词、对象克隆、对象序列化等。
# 1.2内存分配
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
对象所需的内存大小在类加载完成后便可确定,为对象分配内存等同把一块确定大小的内存从java堆中划分出来。
# 1.2.1 内存分配有哪些方式?
分配内存的方式有 指针碰撞 和 空闲列表 2种。
选择哪种分配方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否有压缩整理功能决定。
# 1 指针碰撞
适用场合:堆内存规整(即没有内存碎片)的情况下。
原理:所有用过的内存全部整合到一边,没有用过的内存放在另外一边,中间有一个分界指针,只需要向着没用过的内存方向将指针移动对象内存大小位置即可。
使用该分配方式的GC收集器:Serial,ParNew。
# 2 空闲列表
适用场合:堆内存不规整的情况下。
原理:JVM维护一个列表,该列表会记录哪些内存块是可用的,在分配内存的时候,找到一块足够大的内存块划分给对象实例,最后更新列表记录。
使用该分配方式的GC收集器:CMS。
# 1.2.2 内存分配的并发问题怎么解决?
创建对象是很频繁的问题,需保证线程安全。目前,虚拟机采用了2种方式来保证线程安全:
# 1 CAS+失败重试
CAS操作失败就重试,直到成功为止。保证更新操作的原子性。
# 2 TLAB
缓冲区(Thread Local Allocation Buffer)
JVM为每一个线程预先在Eden区分配一块内存,给线程中的对象分配内存时,先在TLAB上分配,当对象大于TLAB的剩余内存或TLAB的内存用尽时,采用 CAS+失败重试 的方式分配内存。
# 1.3 初始化零值
内存分配完成后,JVM将分配到的内存空间都初始化为零值。
这一步保证了对象的实例字段可以不赋初始值就直接使用。
# 1.4 设置对象头
初始化零值完成后,JVM开始设置对象头。
对象头主要包括2部分信息(如果是数组对象,还有一个数组长度):
1.哈希码、GC分代年龄、锁状态标识等。
2.指向类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。
# 1.5 执行init方法
执行init方法,对应到语言层面上讲,就是为属性赋值(与上面的赋零值不同),和执行构造方法。
# 2 对象栈上分配
通过创建对象时的内存分配可以知道JAVA中的对象都是在堆上进行分配,需要依靠GC进行内存回收,如果对象数量较多的时候,会给GC带来较大压力,间接影响应用的性能。
JVM通过逃逸分析来确定方法内的对象会不会被外部访问(即会不会“逃逸”),如果不会逃逸就可以在栈上给该对象分配内存,这样该对象所占用的内存空间就可以随着栈帧的出栈而销毁。
栈上分配一般同时依赖逃逸分析和标量替换。
# 2.1 对象逃逸分析
就是分析方法内对象是否会被外部引用(是否逃逸),未逃逸的对象在栈上分配内存。
JDK7之后默认开启逃逸分析。
# 2.2 标量替换
标量和聚合量:
1.基础类型和对象的引用可以理解为标量,它们不能被进一步分解。
2.能被进一步分解的就是聚合量,比如:对象。
标量替换:将对象成员变量分解成分散的变量,这些分散的变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
# 3 对象的内存布局
对象在内存中的布局可以分为3块区域:对象头、实例数据和对齐填充。
# 3.1 对象头
对象头主要由3部分组成:Mark Word、指向类的指针、数组长度(只有数组对象有)
# 1 Mark Word
Mark Word记录了哈希码、GC分代年龄、锁标识等信息。
在32位的JVM中长度是32bit,在64位的JVM中长度是64bit。
# 2 指向类的指针
JAVA对象的类数据保存在方法区。
该指针在32位的JVM中长度是32bit,在64位的JVM中长度是64bit。
# 3 数组长度(只有数组对象有)
该对象在32位和64位的JVM中长度都是32bit。
# 3.2 实例数据
对象真正存储的有效信息,也就是程序中定义的各种类型的字段内容。
# 3.3 对齐填充
对齐填充不是必然存在的,只是因为Hotspot要求对象大小必须是8字节的整数倍。
而对象头部分是8字节的倍数(1倍或2倍),所以实例数据部分没有对齐时,需要对齐填充来补全。
# 4 对象的访问定位
JAVA程序通过栈上的reference数据来操作堆上的具体对象。由于reference类型在JVM规范中只定义了一个指向对象的引用,没有说明这个引用通过何种方式去定位、访问堆中对象的具体位置,所以访问的方式取决于JVM的实现。目前主要的访问方式由2种:句柄和直接指针。
# 4.1 句柄
JAVA堆中会划分出一块内存来作为句柄池,reference存放的是对象的句柄地址,而句柄中包括对象实例的地址和对象Class的地址。
# 4.2 直接指针
reference存放的是对象实例地址。对象实例数据的对象头里包含Class的地址。
# 4.3 两种访问方式各有什么优势?
1.句柄访问最大的好处是reference中存储的是稳定的句柄地址,对象移动时只会改变句柄中的实例数据指针,reference不需要修改。
2.直接指针访问的好处是速度快,减少了一次指针定位的时间开销。