正文
Java作为一门面向对象的语言,那么它的对象是怎么创建的了?
流程
1、类加载检查
虚拟机遇到一个 new 指令的时候,就会去确认这个类是否已经被加载,相关类加载内容,可以参考https://www.jianshu.com/p/2064f8ee8c90。
new 指令在语言层面上讲的是,new 关键字,对象克隆,对象序列化等。
2、内存分配
对象所需内存在类加载完成后可以完全确定,所以内存分配就是在 Java 堆中划分出指定内存大小的空间。
这个过程存在两个问题:
1、如何划分内存。
2、在并发情况下,对象 A 分配完内存,指针未修改,对象 B 分配内存,又使用了原来的指针分配了内存。
划分内存
-
指针碰撞(Bump the Pointer)
前提条件是堆空间是绝对规整,可以通过一个指针将堆空间划分为已使用内存和未使用内存,每次生成新的对象,该指针往后挪即可。 -
空闲列表(Free List)
当堆空间不是规整的时候,需要通过一个列表维护内存位置使用情况。 -
选择
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。
并发问题解决方案
-
CAS(Compare And Swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。失败重试的内存位置会发生改变。 -
本地线程分配缓存(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过XX:+/UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。
3、初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB,该步骤可以提前至 TLAB 分配的时候进行。这一步确保了对象的实例字段在 Java 中不需要赋初始化值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
4、设置对象头
初始化零值后,虚拟机需要对对象进行必要的设置(我觉得相对于对象可以认为全局的属性),比如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的 HASH 码、对象的 GC 分代年龄等信息。这些信息都是存在对象的对象头 Object Header中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在设置对象头的时候,会涉及到指针压缩(比如原本一个 String 的指针是8位,压缩到4位):
什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针
3.启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,
占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm
只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内
存不要大于32G为好
5、执行<init>方法
执行<init>方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。