3.对象的创建

3对象的创建

对象创建过程

类加载->对象空间分配->设置对象头->对象初始化

类加载

  • java类加载连接初始化是在程序运行期间完成的。

  • 类的生命周期:加载验证 准备 解析(都属于连接过程),初始化使用卸载

  • 解析可能在初始化之后,其他阶段可能是交叉进行的。

  • 初始化开始时间

    • new调用静态方法反射调用初始化子类的时候先触发父类的初始化先初始化main方法在的主类使用动态语言支持时
    • 注:以上为有且只有,因此例如通过子类调用父类的静态变量时不会初始化子类,只初始化父类、声明对象数组不会导致初始化,调用类中的常量也不会导致初始化。
    • 接口初始化与类的区别:同类初始化时间相同,但当子接口初始化的时候不需要父类接口初始化。
  1. 加载

    • 通过类的全限定名获得class文件的二进制流-》将字节流转化到方法区中-》生成一个代表此类的class对象代作为方法区这个类的各种数据的访问入口(此对象不一定存在堆中可能在方法区)。
    • 数组类的加载不同,数组类由jvm创建不经过类加载器,如果数组里存储的是引用类型则调用此引用类的加载过程,并将数组类标识到此引用类的类加载器上。如果不是引用类型而是基本类型如int[]数组则将数组类标记到引导类加载器上。数组类可见性与数组中的元素类可见性一致,基本类型则默认为public。
    • 加载与验证交叉进行,验证可能在加载的过程中开始。
  2. 连接-验证

    • 确保class字节流中的信息是安全的符合要求的。因此是可关闭大部分的验证过程的。

    • 文件格式验证->元数据验证->字节码验证->符号引用验证

    • 文件格式验证

      • 主要目的保证字节流能正确解析并存储于方法区。通过此验证后才会进入方法区存储。
      • 是否以魔数开头(以此确认是否为class文件),主次版本号是否可在当前jvm上运行,常量是否有不被支持的类型(检查tag标志),指向常量的索引值中是否有指向不存在的常量或不符合类型的常量,是否有被删除或被附加的其他信息。等等
    • 元数据验证

      • 对字节码中的信息进行语义分析(数据类型校验),保证符合java语言规范。
        • 是否有父类(除了object类所有类必须由父类)
        • 是否继承了不允许被继承的类
        • 是否实现了父类或接口中要求实现的方法(抽象类接口除外)
        • 是否存在不合规的重载或覆盖
    • 字节码验证

      • 确定程序语义合法,方法体验证.
        • 栈中的数据类型与指令代码序列对应
        • 跳转指令不会跳出方法体
        • 方法体中的类型转换是有效的b
    • 符号引用验证

      • 符号引用转化为直接引用时进行的验证,在解析阶段发生
      • 对类自身以外的信息(常量池中的符号引用)进行匹配验证
        • 符号引用中的全限定名是否可以找到对应的类
        • 指定类中是否存在符合描述的方法和字段
        • 符号引用中的类、字段、方法的访问性(privatepublic)是否可以访问
  3. 连接-准备

    • 正式为类变量分配内存并设置类变量初始值(此初始值为类型初始值非定义的初始值)的阶段,这些变量分配到方法区。
    • 类常量会直接赋予用户定义的值而不是类型初始值。
  4. 解析

    • 符号引用转换为直接引用的阶段。
      • 符号引用
        • 用符号来表示引用的目标,引用的目标可能还没加载到内存,与内存布局无关。
      • 直接引用
        • 直接指向目标的指针相对偏移量可以定位目标的句柄等,引用的目标已经存在于内存中。
    • 可以根据需要判断是在类加载时进行解析或等到被使用时再解析。
  5. 初始化

    • 初始化阶段才是真正在执行类中的代码。准备阶段已经为类变量赋了系统默认初始值,初始化阶段将负责为类变量赋上java代码中定义的值。或者说是执行类构造器中的clinit方法。

      • clinit方法是由编译器生成的。

      • clinit方法的组成

        • 由java代码中的静态代码块和类变量定义的顺序来组成

        • static {
              i=0;
              System.out.println(i);//错误示范
          }
          int i=2;
          //注意此类中静态代码块在前,静态代码块中引用的变量在代码块后定义,但是因为变量是在准备过程中分配的空间,因此可以对i进行赋值,但是不可以进行访问。
        • 父类的clinit方法先执行,子类的后执行。注:但父接口中的clinit方法只有在使用父类的变量时才执行。

    至此类加载过程结束。

  6. 类加载器

    • jvm把加载过程中的通过类的全限定名获取二进制流的动作放到jvm外部实现,因此可以自定义加如何去获得类的二进制流。

    • 类加载器的功能除了加载以外还用于确定一个类的唯一性。类的唯一性由类加载器和它本身来一同确定。类的唯一性会导致class对象的equals方法,isAssignableFrom方法,isInstance方法,instanceof关键字的返回结果。

    • 类加载器的分类

      • 启动类加载器Bootstrap类加载器,c++实现。在java中用null代替。
      • 其他类加载器,java实现,继承自java.lang.ClassLoader。
        • 扩展类加载器
        • 应用程序类加载器
        • 用户自定义的类加载器
      • 双亲委派模型
        • 类的加载委托给此类加载器的父类加载器,若父类加载器失败则由此类加载器再进行加载。
        • 被破坏:如jndi服务,它是由启动类加载器加载但其需要调用的代码是在classpath下的因此启动类加载器无法加载,便引入了线程上下文加载器来破坏双亲委派模型。

对象空间分配

  • 在类加载过程之后便开始为新对象在堆上分配内存,内存大小在类加载时便确定,因此只需在堆中划分出内存空间便可。

  • 内存空间分配:

    • 指针碰撞
      • 若堆是规整的则采用一个指针,指针左侧是已经分配了的对象,右侧是空闲的空间。
    • 空闲列表
      • 若堆是不规整的,则采用空闲列表存储可用的内存区域,分配时从中选出符合大小的内存区域。
    • 指针碰撞还是空闲列表取决于垃圾收集器是否带有整理功能。
    • 因为分配内存是线程不安全的,所以一般采用cas加上失败重试的方式来保证操作的原子性,并通过本地线程分配缓冲TLAB来提前为线程分配属于自己的空间来避免同步问题。并在属于自己的空间不够时再进行cas+重试来保证扩容的原子性。

对象内存布局

  • hotspot虚拟机中对象的内存布局分为对象头,实例数据,对齐填充三个部分。
  • 对象头
    • 同虚拟机位数相同的MarkWord区域,用来存储运行时数据.
      • 如:哈希码,gc年龄,锁状态等等。
    • 类型指针,对象指向它的类元数据的指针。(是否存在指向类元数据的指针取决于虚拟机实现方式,存在不通过对象来寻找对象对应的类信息的方式)
  • 实例数据
    • 代码中定义的各种类型的内容,一般分类按顺序放置。包括父类的中继承的数据。
  • 对齐填充
    • 对象内存必须整数个bit。

对象初始化

元数据的指针取决于虚拟机实现方式,存在不通过对象来寻找对象对应的类信息的方式)

  • 实例数据
    • 代码中定义的各种类型的内容,一般分类按顺序放置。包括父类的中继承的数据。
  • 对齐填充
    • 对象内存必须整数个bit。

对象初始化

  • 执行对象的init方法来对对象中的字段进行赋值初始化。此前字段均为0。
发布了27 篇原创文章 · 获赞 1 · 访问量 901

猜你喜欢

转载自blog.csdn.net/hu853996234/article/details/103736343