当前位置: 首页 > 科技观察

JVM源码分析-对象创建过程

时间:2023-03-11 20:29:23 科技观察

在开始学习MySQL之前,我也想写一篇文章来回顾一下前面学习的知识点,今天的文章就到这里了。例子有类School,它有3个成员变量:引用类型String类型的schoolName,通过显式代码块初始化;基本数据类型int类型studentsNum,显式初始化;引用类型Class类型student,通过School的构造函数初始化。我们使用main函数创建了一个School的对象,那么这个过程中发生了什么?JVM内存中还有什么?让我们来看看!publicclassSchool{privateStringschoolName;privateintstudentsNum=10000;privateStudentstudent;{schoolName="清华大学";}publicSchool(){student=newStudent();}}classStudent{}classTest{publicstaticvoidmain(String[]args){Schoolschool=newSchool();}}当我们执行newSchool()时,对象被创建,大致可以分为以下5个步骤:在详细理解这5个步骤之前,我们先详细说一下对象头。我们在分析synchronized锁升级过程的时候已经接触过了。对象的内存布局对象在堆空间的内存布局包括三部分:对象头(Header)、实例数据(IntanceData)、对齐填充(Padding)。对象头对象头包含两部分:运行时元数据、指向类元数据的指针kclass,以及确认这个对象的类型。运行时元数据(MarkWord)包括:哈希值、GC分代年龄、锁状态标志、偏向线程ID。元数据信息在运行时发生变化。在同步锁的升级过程中,不同锁状态下的MarkWord是不同的。下图展示了无锁状态、偏向锁、轻量级锁、重度锁以及被GC标记的对象头的运行时数据信息:InstancedataInstancedata是对象实际存储的有效信息,它包含了对象中定义的各种类型的字段。这些字段要么由对象本身定义,要么从所有父对象继承。父类的构造方法在子类之前执行,所以父类的变量定义都是在子类之前定义的。对齐填充对齐填充不是必需的,也没有实际意义,它只是一个占位符。HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,所以当对象不满足时,需要对齐填充来完成。既然了解了对象在堆内存中的布局,那么我们在之前的JVM文章中也了解了虚拟机栈结构和方法区(JDK1之后称为metaspace,为了方便混淆,后面用metaspace来表示)),接下来我们将详细分析创建学校对象的整个过程。对象创建的步骤对象的创建是在主线程的main()方法中进行的,所以main()的栈帧会在主线程的虚拟机栈中创建,main()是当前方法。我们回顾堆栈和堆栈框架。JVM内存区分为5个模块:堆、元空间、虚拟机栈、本地方法栈、程序计数器(也叫pc寄存器)。虚拟机栈和本地方法栈都属于栈,本地方法栈中只存储native方法的栈信息。虚拟机栈的生命周期与线程的生命周期是一致的。它随着线程的创建而创建,随着线程的销毁而销毁,因此是线程私有的内存区域。虚拟机栈由栈帧组成,其中包含局部变量表、操作数栈、动态链接、方法返回地址和附加信息。栈帧是通过方法调用创建的。所以当主线程调用main()方法时,此时在主线程的虚拟机栈中创建了main()栈帧。main()栈帧中的局部变量表包含两个变量:args和school。主线程的虚拟机栈的栈帧结构如下:如果main()方法要实例化局部变量school,需要实例化类School。那么newSchool()发生了什么?让我们详细分析一下前面的5个步骤。判断对象的类是否已经加载虚拟机遇到new指令时,首先会检查这条指令的参数是否能在元空间的常量池中定位到某个类的符号引用,并检查该类是否由符号引用表示的已经被加载。待加载,即判断元空间中是否包含本类的类元信息。我们通过javap-v-pTest.clas查看测试类的字节码信息:Classfile/E:/study/javacodegirl/src/main/java/com/study/test/code/girl/base/jvm/Test。classLastmodified2021-2-21;size352bytesMD5checksum2df3d394ac88d2aa4da9d27f848067c5编译自“School.java”classcom.study.test.code.girl.base.jvm.Testminorversion:0majorversion:52flags:ACC_SUPERConstantpool:#1=Method/Object/java#5./#14."":()V#2=Class#15//com/study/test/code/girl/base/jvm/School#3=Methodref#2.#14//com/study/test/code/girl/base/jvm/School."":()V#4=Class#16//com/study/test/code/girl/base/jvm/Test#5=Class#17//java/lang/Object#6=Utf8#7=Utf8()V#8=Utf8Code#9=Utf8LineNumberTable#10=Utf8main#11=Utf8([Ljava/lang/String;)V#12=Utf8SourceFile#13=Utf8School.java#14=NameAndType#6:#7//"":()V#15=Utf8com/study/test/code/girl/base/jvm/School#16=Utf8com/study/测试/code/girl/base/jvm/Test#17=Utf8java/lang/Object{com.study.test.code.girl.base.jvm.Test();descriptor:()Vflags:Code:stack=1,locals=1,args_size=10:aload_01:invokespecial#1//Methodjava/lang/Object."":()V4:returnLineNumberTable:line28:0publicstaticvoidmain(java.lang.String[]);描述符:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=2,locals=2,args_size=10:new#2//classcom/study/test/code/girl/base/jvm/School3:dup4:invokespecial#3//Methodcom/study/test/code/girl/base/jvm/School."":()V7:astore_18:returnLineNumberTable:line30:0line31:8}SourceFile:"School.java"main()中新增命令的参数为#2,如果没有可以在Constantpool中找到#2对应的class信息关于这个类的信息,那么School类将根据双亲委派模型加载。类加载过程:加载、连接、初始化,其中连接包括:验证、准备、分析。是执行类加载的类加载器,分为:启动类加载器、扩展类加载器、应用类加载器、自定义加载器。School类是ClassPath下的一个文件,它的类加载就是应用类加载器。当应用类加载器根据ClassLoader+包名+类名查找对应的.class文件时,如果找不到该文件,将抛出ClassNotFoundException,如果找到,则加载该类并生成对应的Class对象。这时元空间中就有了School的班级元数据。为对象分配内存空间接下来,需要计算对象占用的空间。除了long和double之外的基本类型都是8个字节,byte和boolean是1个字节,char和short是2个字节,其他基本类型都是4个字节,引用类型也是4个字节。计算出内存大小后,在堆中为新对象分配一块内存空间。大多数情况下,对象都分配在新生代的Eden区。如果此时Eden区没有足够的内存空间可供分配,虚拟机就会发起一次MinorGC。但是当我们为一个很长的字符串或者数组分配内存时,这类大对象需要连续的内存空间,可以直接在老年代分配,这样可以避免大量内存在Eden和两个S区copy。但是大对象可能会导致连续空间不足,提前触发GC。我们还应该在开发过程中尽量避免大对象。内存分配有两种方式:指针冲突和空闲列表分配。指针碰撞:当内存使用的GC算法是标记或复制算法时,内存是有规律的。这时候我们只需要移动指针位置,就可以为对象分配内存。Serial和ParNew使用的GC回收算法是标记复制算法,内存分配是指针碰撞的方式。freelist分配:当内存使用的GC算法是mark-and-clear算法时,内存是有规律的。这个时候维护一个空闲内存列表。为新对象分配内存时,从空闲列表中找到内存就足够了。CMS使用的GC回收算法是mark-sweep算法,内存分配方式是freelist分配。看完内存分配,你有没有疑惑?堆内存由所有线程共享。如果两个线程要同时占用这个内存空间怎么办?这涉及到分配内存空间时的并发安全问题。JVM提供了两种处理并发安全的方式:一种是我们常见的CAS失败重试+区域锁来保证内存分配的原子性,另一种是通过开启-XX:+UseTLAB参数A来预分配每个线程TLAB,这个参数在JDK1.8中是默认开启的。经过这一步,堆内存中就有了School实例的内存区域:初始化分配的内存空间属性的赋值操作分为三种,我们在例子中有示例:默认值初始化显式初始化的初始化初始化和代码块初始化构造函数分配的内存空间为默认值初始化,为类成员变量设置默认值,保证对象实例字段在未赋值时可以直接使用。原始数据类型默认值为0,布尔类型默认值为false,引用类型默认值为null。不要把这一步的初始化和类加载过程中的初始化搞混了!类加载过程中的初始化是初始化类的静态变量,不包括类的实例变量。这一步执行完后,内存中的情况是这样的:设置对象的对象头,在对象头中存放对象的类、对象HashCode值、对象GC信息、锁信息等数据。这取决于JVM实现。objectheader的信息我们已经提到了,这里不再赘述。这一步执行完后,内存中的数据发生变化:执行init进行初始化。这个时候,初始化过程才真正开始。这个过程对应字节码invokespecial,执行init方法。它会执行实例化代码块,调用类的构造函数,将对象在堆中的首地址赋值给引用变量。经过这一步,才算是创建了真正可用的对象。执行这一步后,内存的变化如下:总结对象创建过程:加载类元数据->分配内存空间并解决并发问题->初始化分配的内存空间->设置对象头信息->执行init方法进行初始化。在对象的整个创建过程中,需要对JVM的内存区域有更深入的了解,熟悉每个区域存储的数据,知道数据存储在哪个进程中。类的加载元数据是元空间的数据源。我们也可以回顾一下类加载机制,双亲委托模型,哪些场景需要打破双亲委托。之前Gogo分析了JDBCSPI机制,使用线程上下文类加载器打破双亲委托。父母代表团。对象的创建是基于堆空间的。我们可以回顾一下堆空间的内存分配、GC回收算法和GC收集器。设置对象头信息,我们需要了解对象头,也可以根据对象头的数据变化,回顾同步锁的升级过程。创建对象后,内存中的数据变化如下: