一.对象的创建
1. 对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否在常量池中定位到一个类(class)的符号引用,并且检查这个符号引用代表的类(class)是否已经被加载、解析和初始化。如果没有,就必须先执行相应的类加载过程。
类加载检查通过之后,JVM为新生的对象分配内存,对象所需内存的大小在类加载完成后便可完全确定。分配内存等同于把一块确定大小的内存从Java堆中划分出来。
JVM给对象分配内存的两种划分方法:
依据:JVM堆中的内存是否规整。
(1)指针碰撞。如果Java堆中的内存是规整的,所有用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界点的指示器,如果新的对象进来分来,分配内存,就把指针往往空闲内存那边移动,移动距离刚好够新生的对象所需内存。
(2)空闲列表。如果Java堆中的内存不是规整的,JVM维护一个列表,记录那块内存被占用,然后找到一个比较大的空闲内存,把新生的对象内存放进去。
JVM堆中的内存是否规整取决于所采用的垃圾收集器是否带有压缩整理功能决定。
举例:
(1)使用Serial、ParNew等待Compact过程的收集器时,系统采用的分配算法是指针碰撞。
(2)使用CMS基于Mark-Sweep算法的收集器时,采用空闲列表。
2.对象在JVM中创建考虑并发问题
JVM给对象A分配内存,指针还未来得及修改,对象B同时使用了原来的指针来分配内存,创建对象时不是线程安全的。
解决方案:
(1)对分配内存空间的动作进行同步处理——JVM采用CAS配上失败重试的方式保证更新操作的原子性。
保持一致性,可以回退。
(2)把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB).
个人理解:
JVM在创建对象时,第一步需要做的是在常量池中找,看这个类是不是被加载过,这个过程是类加载检查。如果没有被加载过,就继续往下进行,进行分配内存,分配的内存的时候,又要考虑两种情况,一种情况是,Java堆中的内存规整,采用指针碰撞。第二种是,Java堆中的内存不规整,采用空闲列表。分配内存时候,又出现了新的问题,问题是并发的情况下,考虑线程安全。为了解决线程安全又有两种解决方案,第一种是对分配内存空间的动作进行同步处理,第二种是提前根据分配内存的动作,按照线程提前分配好,用到的时候,直接拿去用。
创建对象流程:
类加载检查——>分配内存
(1)分配内存的方式
<1>指针碰撞
<2>空闲列表
(2)分配内存并发线程安全
<1>采用CAS配上失败重试的方式
<2>提前分配好内存,本地线程分配缓冲,虚拟机是否使用TLAB,可以通过-XX:+/-UserTLAB参数设定。
执行完new方法之后,接着执行<init>方法。
二.对象的内存布局(对象存在JVM内存中的存储格局)
1.在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Dat)和对齐填充(Padding)。
HotSpot虚拟机的对象头分为两部分:
(1)用于存储对象自身的运行时数据如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,被官方称为Mark Word,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,会根据对象的状态复用自己的存储空间。
(2)类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个java数组,对象头中还存在一块用于记录数组长度的数据。
HotSpot虚拟机中的实例部分是对象真正存储的有效信息,也是程序代码中所定义的各种类型的字段内容。这两部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。(未理解有何作用)
对齐填充,并不是必然存在的,仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是对象的大小必须是8字节的整数倍。当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
个人理解:
内存里有3块地方给对象,一是对象头,二是对象实例,三是对齐填充。对象头存着一些标志,时间戳等身份认证的标志啥的,它有两个小弟,一个mark word 另一个是类型指针,mark word是仓库搬运工,类型指针是仓库管理员,管理员熟悉常进的货是什么,进过什么货,没进过的货要查一下。对齐填充是个混子,没事来充下人头。对象实例是整个对象的核心,最值钱的,最有用的部分,对于对象实例的排序也有影响,大致就是辈分越大的越往里走,具体需要再理解。
二.对象的访问定位
1.对象的使用
通过栈上的reference数据来操作堆上的具体对象。
reference类型在虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式定位、访问堆中的对象的具体位置,对象访问方式取决于虚拟机实现而定。主流访问方式有使用句柄和直接指针两种。
注:两点。(1)reference类型从栈指向堆,明确表达是reference类型;(2)怎么使用reference类型是取决我们,如何访问有两种主流方式,使用句柄和直接指针。
(1)句柄访问
从Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址。句柄中包含了对象实例数据与类型数据各自的具体地址信息。
Java栈本地变量表(reference)——句柄池(到对象实例数据的指针)——Java堆(对象实例数据)和方法区(对象实例数据)
(2)直接指针
Java栈本地变量表(reference)————Java堆(对象实例数据)和方法区(对象实例数据)
优缺点:
(1)句柄访问,通过句柄代理,对象被移动或者回收时,只需要改变句柄中的实例数据指针,reference本身不需要修改。
句柄像一个接口。
(2)直接访问,访问速度快。