深入理解JVM的对象创建过程
关于对象创建,有很多种方法。比如可以通过反射,或者通过 new关键字来创建。不管是何种方式,最终都是会创建一个对象。而我们平常工作中最常用的就是通过new关键字来创建对象。对于我们而言,只要new一下,就会有一个新的对象供我们使用。但是对于程序,对于虚拟机而言,new一下,它是如何去创建对象的呢?
对于JVM来说,当他碰到new对象的指令时会做如下几个步骤
对象的创建过程
1.判断类是否已经被加载
我们都知道当我们要使用某个类的时候,这个类是要先被编译成class文件,然后再被类加载器加载到内存的,其实就是把类的class文件信息加载到方法区的元空间当中。这些信息可以简单的理解为类的版本、字段、方法、接口等一些描述信息。除此之外,class文件还有一个信息会被也会被加到到元空间。也就是常量池。用于存放编译期生成的字面量跟符号引用
例如JvmStudyDemo生成的常量池,也就是下图标红的区域
实际上当JVM发现你要new的对象的类已经被加载过了以后就会继续往下执行,如果还没被加载,那么会先加载,然后执行接下来的步骤。
关于类的加载这一篇文章有描述 JVM根据源码深度解析类加载器,以及类加载器的原理
2.分配内存
当类已经被加载完毕了,那么会执行第二步,也就是分配内存。我们都知道new对象一般情况来说生成的对象都是会存放在堆当中(当存在栈上分配时,逃逸对象会优先分配在栈当中),那么存放肯定是需要内存空间去存放对象的。这里就涉及到两个问题。
问题1 如何分配内存?
分配内存的方式有两种。第一种是指针碰撞,第二种是空闲列表。
指针碰撞
指针碰撞的前提条件是堆中的内存是规整的,也就是说没有内存碎片的产生。因为对象实际上是以连续的内存空间去存放的。所以,当内存规整的时候,通过指针碰撞的方式就可以更加充分的利用内存。这里画一张图给大家理解一下。
堆中内存是绝对规整的,所有用过的内存都被放在了一边,没有用过的内存放在另外一边。中间通过一个指针来进行划分。当有新new的对象要在堆中划分内存时,这个指针会向空闲内存空间偏移一段可以存放下新对象的内存地址,然后再将新的对象存放到刚刚划分出来的新的内存空间当中。
空闲列表
空闲列表的方式是在内存不规整的情况下的一种内存的分配方式。如下图
空闲列表是指,堆中可用空间跟已经使用的空间都相互交错,就没有办法通过指针碰撞这种方式来进行内存分配。这个时候虚拟机会维护一个列表去记录堆当中大大小小的可用内存空间,当新的对象需要进来分配内存空间的时候,会从空闲列表中找到一块能够存放进新对象的内存区域去存放对象,并且更新空闲列表的记录。
通过这两种方式,我们了解了JVM分配内存的机制。但是这里有一个问题,我们从一开始就在讨论规整的内存与不规整的内存的内存分配方式,但是大家有没有想过堆中的内存规整不规整这个又是由什么导致的呢?
其实这个跟使用的垃圾回收器有关。关于垃圾回收器的我会另外再开一篇文章来讲。
问题2 并发情况下如何去处理内存分配?
创建对象肯定是会发生并发情况的,当某个线程调用的方法在创建对象的时候,他并不知道这个时候会不会有其他线程在这个时候恰巧也在创建对象。这就会产生并发争抢内存的现象。
JVM针对这种现象也给出了相应的解决措施,一种是CAS,另外一种则是TLAB。
CAS(compare and swap)
通过CAS + 失败重试,保证以原子性的方式来对分配内容的动作进行同步处理。
TLAB(Thread Local Allocation Buffer)
TLAB翻译过来叫做本地线程分配缓冲区。是指把内存分配的执行按照线程划分到不同的空间之中进行,也就是说每个开启的线程都会在堆中事先分配一小块内存空间,用这一块空间来存放对象。也就避免的多个线程同时分配对象内存的资源争抢的问题。
JVM默认是开启了TLAB
可以通过 -XX:+/-UseTLAB来决定开启或者关闭,还可以通过-XX:TLABSize指定每个线程的缓冲区大小。
3.初始化
当内存分配好了之后,虚拟机就会将分配到的内存空间都初始化为零值,但是这一过程不包括对对象头的初始化。这一步完成之后,就能够保证访问对象的没有赋值的字段也可以被我们使用了。比如int类型的字段初始值是0,这个初始值可以被我们程序所访问到。
4.设置对象头
对象头是一个非常重要的属性。例如这个对象是哪个类的实例,如何才能找到类的元数据信息,以及对象的哈希码,对象的GC分代年龄等等。这些信息都存放在对象的对象头之中。
虚拟机有很多种,但是我们市面上用的最多的就是HotSpot虚拟机。HotSpot虚拟机的对象在内存中的存储可以划分为三部分。第一部分是对象头(Header),第二部分是实例数据(Instance Data),第三部分是对齐填充(Padding)
5.执行init方法
最后也就是执行init方法了,有看过反编译的class字节码文件的同学就会知道,new指令在class字节码文件中会紧跟着一个init方法,这个就是对对象的属性进行一个赋值操作了
这里可以截个图给大家看一下
那么到这里,整个的对象创建过程已经结束了。一个新的对象被new了出来。
现在你还觉得new一个对象是一个简单的操作了吗? 其实对于程序而言,一点都不简单。
我这里还没有深入展开去讲内存分配的各种机制
比如栈上分配,大对象,长期存活对象,分代年龄判断机制,老年代空间分配担保机制。
这一些机制都将是我们进行JVM调优的重要参考机制。在创建对象这一章先不展开讲,先给大家提一笔。