一、对象创建的流程
- 在java中当需要创建一个对象的时候,通常使用如下的方法创建一个对象:
Object object = new Object();
那当jvm执行这条语句的时候的在其内部是怎样的一个数据流程呢?在每一个流程中都做了哪些工作?为了解决这个上述的问题。我们先来看看如下的一张对象的生命周期流程图:
二、何时会出发一个对象的创建
1.当我们使用new关键字去准备去实例化一个对象的时候。就会出发对象的创建。如下的代码段是创建一个简单的Object对象的代码:
public class Math {
public static void main(String[] args) {
Object object = new Object();
}
}
对上述的代码经过javap指令反编译得到的字节码如下:
public class com.milkcoffee.jvm.objectcreateandmemoryalloctedmechanism.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/Object
#3 = Class #22 // com/milkcoffee/jvm/objectcreateandmemoryalloctedmechanism/Math
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/milkcoffee/jvm/objectcreateandmemoryalloctedmechanism/Math;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 object
#16 = Utf8 Ljava/lang/Object;
#17 = Utf8 MethodParameters
#18 = Utf8 SourceFile
#19 = Utf8 Math.java
#20 = NameAndType #4:#5 // "<init>":()V
#21 = Utf8 java/lang/Object
#22 = Utf8 com/milkcoffee/jvm/objectcreateandmemoryalloctedmechanism/Math
{
public com.milkcoffee.jvm.objectcreateandmemoryalloctedmechanism.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/milkcoffee/jvm/objectcreateandmemoryalloctedmechanism/Math;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
// 执行创建对象的逻辑
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 object Ljava/lang/Object;
MethodParameters:
Name Flags
args
}
通过反编译之后的字节码知道实际上我们在创建对象的时候。jvm实际上是通过使用这个指令来完成的invokespecial。那这个指令又有何含义呢?如下:
invokespecial:用于三种场景:调用实例构造方法,调用私有方法(即private关键字修饰的方法)和父类方法(即super关键字调用的方法
结合上述的代码实际上就是的调用实例的构造方法。到此我们也就知道当我们使用new关键字创建一个对象的时候jvm是使用invokespecial调用对象的构造方法实现的。
三、对象所关联的类是如何加载
前面我们说到我们可以使用new关键字创建对象。但是呢要创建一个对象首先就需要找到对象对应的描述文件。在Java里面我们使用类来描述一个对象它应该有哪些属性执行哪些操作。那在创建对象之前我们肯定需要先将对象对应的类加载到jvm中方法区中。那类是如何被加载到jvm中的呢?jvm是通过其自身的类加载子系统实现的类的加载。在jvm中内置的类加载器:启动类加载器,在jvm的进程启动的时候由jvm底层的c++代码负责去初始化。扩展类加载器以及应用类加载都是在java中的Launcher类中进行初始化的。这些加载器对类的加载遵守一种双亲委派的机制,简单说就是先让父加载器加载,父加载器没有加载成功再让子加载器加载。
四、对象所需的内存空间分配策略
- 在类加载完成之后,对于每一个对象需要多少的内存空间用来存储数据就已经确定了。接下来jvm就要在自己的堆区中将一块内存划分给这个对象使用。那jvm有哪些内存的划分机制:
- 指针碰撞:简单来说这种方式就像我们平时操作指针一样按照一定的规则移动。但是这种分配方式的前提是内存必须是规整的。已经被使用的内存是放在一边没有使用的内存是放在另外一边。在分配的时候从使用和没有使用的交界处向没有使用的那边移动。如下所示:
- 空闲列表:实际上就是使用一块额外的内存空间存储当前内存中哪些内存是还没有被分配的以及对应的内存的大小。那既然已经有指针碰撞的分配方式了为何还需要使用这种方式呢?其实这样想一想假如第一轮是使用的指针碰撞的方式将内存分配出去,在对象被使用完成之后就会被垃圾回收器回收内存。回收之后的内存已经不是规整的而是东一块西一块的。指针碰撞的方式已经不能在使用,那最好的解决方式就是使用一个小本记录下那些内存已经被使用哪些没有被使用,在内存被分配之后在重新更新这个列表。如下的图所示:
上述的两种方式在单线程的情况下分配内存没有任何问题。但是在多线程并发的情况下可能就会存在问题:我们都知道在多线程的环境下导致并发问题主要是由于:原子性、可见性、有序性引起的。在JVM中给对象分配内存也会存在相同的问题。我还在修改指针或者修改空闲列表的时候时间片使用完了发生线程的切换,当再一次回来的时候这块已经已经被分配给其他的线程,同一块内存放两个不同的数据是一定会存在问题的。那JVM是如何来解决这个问题的呢?
- CAS:JVM使用CAS加失败重试的方式解决并发的内存分配问题。既然是使用CAS那就有可能存在ABA问题,但是限于本人水平问题没有找到这个方面的答案。
- 本地线程分配缓冲:也就是每一个线程会在Java的堆中预先分配一块内存。使用JVM参数XX:+UseTLAB指定是否使用本地线程分配缓冲。这块内存的大小是使用参数XX:TLABSize指定的
五、对象数据的初始化
到此为止内存就算已经分配到了。既然都已经分配到内存了那接下来就是需要对分配到的内存初始化为JVM默认的零值。如Long初始化为0L、对象初始化为null;
六、设置对象头
何为对象头呢?说起对象头这个还得从对象说起。我们想想当使用new关键字创建一个类的实例的时候,在使用的过程中是如何知道我们哪一个类的实例的呢?在使用内置锁的时候是如何知道当前持有锁的是哪一个线程呢?等等这些问题都是使用对象头来记录的。一个完整的对象包括如下的几个部分:
-
对齐填充:那为何需要对齐填充呢?当我们创建对象的时候不同的类创建出来的对象的大小是不一样的。在java中64位是8字节对齐。这样做的目的是使用空间换取时间的做法。这样想一想假如没有对齐cpu从内存加载数据的时候还需要计算一下这个对象的开始地址是多少结束地址是多少,这样无疑是拉低了cpu的处理的效率。采用对齐之后只需要使用具有相同规律的内存地址就可以取出对应的对象。除此之外之外对齐还有利于垃圾收集器的工作。
-
实例数据:就是对象中的数据
-
对象头:对象头包括Mark Word标记字段(32位的机器占4个字节、64位的机器占8个字节)自身运行时数据:hashcode、GC分带年龄、锁状态标志、持有锁的线程、偏向的线程id、偏向的时间戳。Klass Pointer类型指针(开启压缩时4字节、关闭压缩时8字节)类的元数据指针。数据长度。
-
那一个对象使用图表示就是如下的几个部
看到的上面的对象构成图那我们就有必要解释一下这几个概念:
-
Klass Pointer类型指针和我在使用Object.class得到的类的Class有何区别?
- KlassPointer是在JVM层面的是对象中指向方法区类元数据信息的一个引用,是由jvm自己去操作对外部是不可见的。如下图所示:
- 那使用这样的方式获取的class呢
Class<? extends ScheduledThreadPoolExecutor> executorClass = scheduledThreadPoolExecutor.getClass();
这个是jvm对类的klass的一个对外的封装方便java开发人员去操作这个对象。可以理解为user元数据的一个镜像。
- KlassPointer是在JVM层面的是对象中指向方法区类元数据信息的一个引用,是由jvm自己去操作对外部是不可见的。如下图所示:
-
通过java代码看看jvm内部对象是如何对齐的
public class ObjectSample {
public static void main<