对象的创建
概述
Java是一门面向对象的语言,Java程序运行过程中无时不刻都有对象被创建出来。在语言层面上,创建对象(克隆、反序列化)就是一个new关键字而已,但是在虚拟机层面却不是如此,看一下在虚拟机层面上创建对象的步骤
类加载检查
- 当JVM检测到有一条new指令时,首先先检查该指令的参数是否在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已被加载、解析和初始化过。
- 如果存在的话,JVM将直接使用已有的信息对该类进行操作。
- 如果不存在的话,则执行相应的类加载过程。
虚拟机为新生对象分配内存
- 类加载检查通过后,虚拟机为新生对象分配内存,对象所需的内存大小在类加载完成后便可以完全确定,为对象分配空间无非就是从Java堆中划出一块确定大小的内存而已。
- 不同的JVM垃圾收集器在分配内存时的表现也不相同,具体两种
- 指针碰撞
- 如果垃圾收集器选择的是Serial、ParNew这种基于压缩整理算法的,那么内存时规整的,虚拟机将采用指针碰撞法来为对象分配内存。意思是加入整个内存是一整块,所用过的内存在一边,空闲的内存在另一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是吧指针往空闲那边挪动一段与对象大小相等的距离罢了。
- 空闲列表法
- 如果垃圾收集器选择的是CMS这种基于标记-清楚算法的,那么内存不是规整的,已使用的内存和空闲的内存相互交错,虚拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,列表记录上哪些内存块是可用的以及内存块的大小和位置,再分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表上的内容。
- 指针碰撞
- 具体使用指针碰撞还是空闲列表法是由Java堆内存是否规整来决定的,内存是否规整是由垃圾回收策略决定的。如果垃圾收集器采用了压缩整理算法,那么在回收垃圾整理内存的时候,就会将内存规整,就可以使用指针碰撞。反之则不可以
- 线程安全性
- 这样就存在一个问题,new对象的线程安全性,也就是内存分配时的同步问题。因为可能存在虚拟机正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
- 这种情况下虚拟机会通过两种方式进行同步:
- CAS和失败重试机制:对分配内存空间的动作进行同步处理,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。CAS简单解释就是:比较并交换,通过3\操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
- TLAB方式:把内存的分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中先预留一块本地线程分配缓冲(TLAB)。哪个线程分配内存时,就在哪个线程的TLAB分配,只有当TLAB用完并分配新的TLAB时,才需要同步锁定。
- 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
对对象进行必要的设置
- 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
初始化对象
- 当完成上述操作后,对象的内存便分配成功了,但是所有的字段都还是零。
- 此时应该执行<init>方法,把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。
下面我们再将上面的过程重新画一张图,总结一下:
对象的内存布局
概述
-
对象在内存中存储的布局分为3个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头(分为两部分信息)
- 第一部分用于存储对象自身的运行时数据
- 如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据在32位和64位的虚拟机上(未开启压缩指针得情况)中分别为32bit和64bit。官方称为"Mark Word"。
- 如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部分数据在32位和64位的虚拟机上(未开启压缩指针得情况)中分别为32bit和64bit。官方称为"Mark Word"。
- 第二部分存储类型指针。
- 对象指向它的类元数据的指针,虚拟机通过这个指针来确定是哪个类的对象。
实例数据
- 存储对象真正的有效信息,也就是程序代码中定义的各种类型的字段内容(无论是父类还是子类中定义的)。
- 存储顺序受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java中定义的顺序的影响。
- Hotspot的默认分配策略:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)。从分配策略可以看出,相同宽度的字段总是被分配到一起。
对齐填充
- 不是必然存在,存在的意义仅仅起着占位符的作用。因为Hotspot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。
对象的访问定位
概述
建立对象是为了访问对象,我们的Java程序需要通过栈(就是虚拟机栈上的局部变量表)上的reference(引用)数据(局部变量表会存放对象的引用地址)来操作堆上的具体对象。关于怎样能够通过reference类型访问对象的具体位置,虚拟机通过何种方式去定位,目前主流的方式有两种
- 通过句柄
- 就是Java堆中会划出一块内存用来作为句柄池,reference中存放的就是对象的句柄地址,而句柄中包含了对象实例数据(该部分的数据主要存放在Java堆中)与类型数据(该部分的数据主要存放在方法区中)各自的具体地址信息。
- 优点:就是reference中存放的是句柄地址,这是非常稳定的,就是对象移动了(垃圾收集时移动对象非常常见,复制算法、标记-整理算法等都会移动对象)也只是会修改句柄中的实例数据指针,而reference不会修改。
- 缺点:速度不会是很快,因为需要多一次指针定位的时间开销。
- 直接指针访问
- 如果使用直接指针访问方式,那么Java堆中对象的布局就必须考虑如果放置访问类型数据的相关信息
- 优点:访问速度更快,节省了一次指针定位的时间开销