Java类加载、new对象时发生了什么?

2020-08-15

类加载

什么是类加载?

java文件经过编译器编译成字节码后,接下来需要经过类装载器,然后再进入JVM内存。而类装载器的作用就是对类进行加载。可以参考下图:

image-20200813145031629

类加载的概念是:

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验转换解析初始化,最终形成可被虚拟机直接使用的Java类型的过程

下面带着问题进入:什么时候会进行类加载?

类加载的过程

类的生命周期分为5个阶段:加载–>连接–>初始化–>使用–>卸载

类的加载包括前三个部分:加载–>连接(验证,准备,解析)–>初始化

下面介绍加载的每个步骤发生了什么:

加载
  • 通过类的全限定名获取其二进制字节流,并将二进制字节流所包含的静态存储结构转化为方法区的运行时数据结构(概括地来说,就是将class字节流文件所包含的类信息读入到jvm方法区)

  • 在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。

验证
  • 这一阶段主要是为了确保被加载的类的信息准确性,保证JVM能够正常运行

  • 共包括四个验证阶段

    • 文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。

    • 元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。

    • 字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出威海虚拟机安全的事。

    • 符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。

  • 对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xverfity:none来关闭大部分的验证。

准备
  • 为静态变量分配内存,这些内存都在方法区分配,所以静态变量都存储在方法区中,这里不包括实例变量,因为实例变量会在对象实例化后一起分配在堆内存中。

  • 为静态变量设置初始值

    • 基本类型默认为0

    • 引用类型默认为null

    • 注意⚠️:这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值,例如

      public static int value = 1; //在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。
      

      image-20200813153245990

解析
  • 主要是将虚拟机常量池中的符号引用转化为直接引用的过程,简单来说就是 jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)

直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化
  • 为静态变量赋设计好的初始值,包括以下:

    1、声明类变量是指定初始值

    2、使用静态代码块为类变量指定初始值

  • JVM负责对类进行初始化

    1、假如这个类还没有被加载连接,则程序先加载并连接该类

    2、假如该类的直接父类还没有被初始化,则先初始化其直接父类

    3、假如类中有初始化语句,则系统依次执行这些初始化语句

    而类初始化的时机又包括:

    • 使用new实例化对象

      加载完以后JVM中就有了该类的元数据,知道这个Class的成员变量和方法等信息,当要new一个类的实例时就会根据这个Class对象去内存中开辟空间,存放该类的实例对象

    • 访问类的静态变量(或赋值)或(调用)静态方法

    • 反射,如Class.forName()

    • 初始化某个类的子类,则其父类也会被初始化

    • 虚拟机启动时,定义了main方法的类先进行加载

    这也解决了上面刚开始所提出的问题!

接下来介绍类的最后两个生命周期发生了什么:

类的使用:

  • 对象实例化:就是执行类中构造函数的内容,如果该类存在父类JVM会通过显示或者隐示的方式先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值,然后在根据构造函数的代码内容将真正的值赋予实例变量本身,然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法

  • 垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收
  • 对象的终结:对象被GC回收后,对象就不再存在,对象的生命也就走到了尽头

类卸载:

类卸载即类的生命周期走到了最后一步,程序中不再有该类的引用,该类也就会被JVM执行垃圾回收

类加载方式

包含三种方式:

  • 启动应用时由JVM初始化加载含有main方法的主类
  • 通过Class.forName动态反射加载,会默认执行初始化块(static{}),但是 Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
  • 通过classLoader加载,不会执行初始化块

双亲委派原则

【待完善】

类加载器的双亲委派原则:

他的工作流程是: 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。这个理解起来就简单了,比如说,另外一个人给小费,自己不会先去直接拿来塞自己钱包,我们先把钱给领导,领导再给领导,一直到公司老板,老板不想要了,再一级一级往下分。老板要是要这个钱,下面的领导和自己就一分钱没有了。(例子不好,理解就好)

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:

可以避免重复加载,父类已经加载了,子类就不需要再次加载更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心api,会带来相关隐患。

参考文档:

https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc

https://baijiahao.baidu.com/s?id=1662931890502685014&wfr=spider&for=pc