类加载的七个阶段

类生命周期的7个阶段

类从被加载到虚拟机内存中开始,到卸载出内存为止。他的整个生命周期包括七个阶段:加载,验证,准备,解析,初始化,使用,卸载7个阶段。其中验证,准备,解析3个部分统称为连接*(Linking)

image.png

阶段顺序

加载,验证,准备,初始化,卸载这五个阶段的顺序是确定的,但是对于”解析”阶段却不一定。它在某些情况下可以再初始化之后再开始,这样做是为了支持java的运行时绑定特性(也称为动态绑定或晚期绑定,具体在后续介绍分派概念时详细讲解)。

加载

什么时候需要开始类第一个阶段”加载”呢?虚拟机规范没有强制束缚。这点交给虚拟机的具体实现来自由把控。“加载loading”阶段是整个类加载的第一个阶段。

加载三步曲儿

1. 通过一个类的权限定名来获取定义此类的二进制字节流(将整个class文件解析成二进制流),此步骤由类加载器完成。这一动作是放在Java虚拟机外部去实现的,以便让应用程序自己决定如何获取所需的类。

2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据区。(将字节流的数据存入运行时数据区)

3. 在内存中生成一个代表这个类的java.lang.Class对象。作为方法区这个类的各种数据的访问入口。(方法区内自己生成一个对象,在多线程中常用的类对象就是它)

 加载的扩展

注意:比如”通过一个类的全限定名来获取定义此类的二进制字节流”没有指定一定得从某个class文件中获取。所以我们可以从zip压缩包,从网络中获取,运行时计算生成,数据库中读取忙活着从加密文件中读取等等。

我们也可以通过JHSDB看到,JVM启动后,相关的类已经加载进入了方法区,成为了方法区的运行时结构。(注意!一旦经历过解析,JVM就会把class文件的数据加载进内存。所以说相关的类已经进入了方法区,成为了方法区的运行时结构了)。

连接的三个步骤

验证

是连接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流中,包含的信息符合虚拟机的要求。并且不会危害虚拟机的自身安全。但从整体上看,验证阶段大致会完成4个阶段的检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。

文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,这一阶段可能包括下面这些验证点:

1. 是否以魔数cafebaby开头。

2. 主,次版本号是否在当前java虚拟机接受范围内。

3. 常量池的常量中是否有不被支持的常量数据(检查常量tag标志)。

4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

5. Utf8 info型的常量中是否有不符合UTF-8编码的数据。

6. Class文件中各部分及文件本身是否有被删除的或附加的信息。

......以上只是一小部分,没必要深入研究。

总结:这个阶段的验证是基于二进制字节流进行的。只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,所以后面三个验证阶段全部是基于方法区的存储结构(内存)上进行的,不会再直接读取,操作字节流了。

元数据验证

我们直接用编译器直接编译出来的.class文件一般没这些问题。但是.class文件的来源很杂。因此再此处再次判断。

元数据:描述类与类之间关系的数据。

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》

1. 这个类是否有父类(除了Java,lang.Object之外,所有的类都应当有父类)。

2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。

3. 类中的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等。)

4. .....

元数据验证是第二阶段,主要对类的元数据信息进行语义验证。保证不存在《Java语言规范》定义相悖的元数据信息。

字节码验证

字节码验证第三阶段是整个验证过程中最复杂的一个阶段。主要目的是通过数据流分析和控制流分析。确定程序语义是合法的,符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕之后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的行为。例如:

1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如不会出现类似于”在操作数栈放置了一个int类型的数据,使用时却按long类型加载入局部变量表中”这样的情况。

2. 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。

3. 保证方法体中的类型转换总是有效的。例如把一个子类的对象赋值给父类数据类型,这是安全的。但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,完全不相干的一个数据类型,则是危险和不合法的。

4. .......

如果一个方法体中的字节码没通过字节码验证,那肯定是有问题的。

符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转换为直接引用的过程。这个转化动作将在连续的三个阶段------解析阶段中发生,符号引用验证可以看做是对类自身以外(常量池中的各个符号引用)的各类信息进行匹配性校验。通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类,方法,字段等资源。本阶段需要校验的内容如下:

1. 符号引用中通过字符串描述的全限定名是否能找到对应的类。

2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。

3. 符号引用中类,字段,方法的可访问性。

4. 是否可被当前类访问。

5. .....

符号引用验证的主要目的是确保解析行为能够正常执行,如果无法通过符号引用验证,将会抛出异常。

验证总结:验证阶段对于虚拟机的加载机制来说,是非常重要,且不是必须要执行的阶段。因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行没有任何影响了。如果程序运行的全部代码(包括自己编写的,第三方包中的,从外部加载的,动态生成的等所有代码)都被反复编译与验证过了,在生产环境的实施阶段就可以考虑用-Xverify:none参数来关闭大部分验证措施,以缩短虚拟机类加载的时间。

准备(给静态变量赋初值)

准备这个阶段是正式为类中定义的变量(被static修饰的变量)分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中进行分配。(为什么静态方法可以使用该类的class对象。因为class对象在类加载时出现。而static则是在准备阶段出现)。

这个阶段容易产生混淆的概念:

1. 首先这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量(成员变量)。实例变量将会在对象实例化时随着对象一起分配在java堆中。

2. 其次,这里所说的初始值”通常情况”下是数据类型的零值。假设一个类变量定义为。

public static int value = 123;

那变量value在准备阶段后的初始值为0而不是123。因此此时还没有开始执行任何Java方法,而把value赋值给123是后续的初始化环节。

解析

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。

符号引用是一种定义,可以是任何字面上的含义。而直接引用就是直接指向目标的指针,相对偏移量。

我就直白一点:直接引用的作用是在我们运行时数据区内部,帮助调用者找到数据的实际内存地址。符号引用就是我们运行时数据区在类加载阶段,还未对类进行布局时,我们通过符号引用访问class文件中数据的实际内存地址,加载进运行时数据区进行布局。布局完后就拥有了所谓的直接引用。可以相对概念来思考。在运行时数据区与class文件之间,符号引用就像直接引用一样。(很明显,当符号引用转换为了直接引用,这个类的类型数据已经进入了运行时数据区,此时的类已经有了“骨架”)。

解析大概可以分为

1. 类或接口的解析

2. 字段解析

3. 类方法解析

4. 接口方法解析

我们经常遇到的异常就与这个阶段有关。

java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)

java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)

java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)

初始化(给静态变量赋代码里的值)

当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域(假如一个类有一个变量public static int a = 10。在准备阶段会赋值为0。而到了初始化阶段会赋值为10)。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。

我们讲到了这里,先提一下什么时候需要进行类加载,什么时候需要进行连接,什么时候初始化呢?

答案是大部分都是直接去触发类加载的。要触发就直接触发类的初始化了。而初始化之前的步骤顺序都是确认的,因此类加载与连接也跟着执行了。当然还有部分特殊的操作只会触发类加载不会触发类初始化,后面会举例罗列。

初始化的条件

初始化主要对一个class中的static{}语句进行操作(对应的字节码就是client方法)。

Static{}语句对于类或者接口而言都不是必须的。如果一个类中,没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<client>()方法。

初始化阶段,虚拟机规范则是严格规定了有且只有6中情况必须立即对类进行”初始化”(加载,验证,准备再次之前就必须开始,解析不一定)。

1. 遇到new(实例化),getstatic(获取静态变量值),putstatic(存放静态变量值)或invokestatic(静态方法调用)这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,生成这4条指令最常见的Java场景是。

(1)使用new关键字实例化对象时。

(2)读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候。

(3)调用一个类的静态方法时。

2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,需要先触发其父类的初始化。

4. 当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类。

5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。则需要先触发其初始化。

6. 当一个接口定义了JDK1.8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个类发生了初始化,那该接口要在其之前被初始化。

猛地一看,好家伙,那也别一条条记了,只要代码跟类沾点边儿就直接初始化呗?别慌。

我们来看一下什么时候不会触发初始化。

虽然确实存在反例,但也不能完全不能归类记忆。以上6种我们统称为主动引用。除此之外的所有引用类型都不会触发初始化。称为被动引用。具体见以下案例:

案例

首先先定义一个父类

image.png

再定义一个子类

image.png

此时,在调用方法中列举5个例子,查看类的加载以及初始化情况。

image.png

案例1(M1方法)

image.png

image.png

由结果可知:如果子类引用父类中的静态字段,会使父类进行初始化,而不会触发子类的初始化(但是子类会被加载)。

image.png

此时父类的class文件遇到了getstatic字节码指令,而子类没有。

image.png

通过给VM添加-XX:+TraceClassLoading,得知子类虽然没有被初始化,但是已经被加载。

总结:子类继承父类但没有重写父类的静态字段。当调用子类.字段时只会触发父类初始化。子类仅加载。

案例2(M2方法)

image.png

使用数组的方式

image.png

并没有与类初始化相关的字节码指令。因此不会初始化。

image.png

通过给VM添加-XX:+TraceClassLoading,得知使用数组的父类虽然没有被初始化,但是已经被加载。

这只是分配了一堆这个数据类型的空间。并没有操作。可以从这个角度去理解。如果需要给这个数组赋值,需要先遍历这个数组然后依次对每个下标进行赋值。

案例3(M3方法)

image.png

打印一个String常量

image.png

没有初始化字节码指令,因此不进行初始化。

image.png

通过给VM添加-XX:+TraceClassLoading,得知父类也没有被加载。

什么原因呢?

image.png

这里说的就是String常量池的变量

image.png

     可以发现在编译Test的时候,就已经把SuperClazz的常量加载到了Test的常量池中。此外可以测试int类型的数据。但是据观察没有在常量池中找到123字面量,但是也不会进行类加载。(所以String常量池真实一个特殊的存在)

案例4

image.png

image.png

image.png

运行发现,触发了初始化,所以必然已经类加载了。

原因:再次证明了String常量池的特殊性。

类加载的线程安全性

其实初始化的时候就是对静态代码块进行赋值static{}。那么如果多线程去同时初始化一个类,此时虚拟机会保证一个类的<clinit>()方法在多线程环境下会被正确的加锁,同步。所以,如果一个类的<clinit>()(也就是static{})方法中有耗时很长的操作,就有可能造成堵塞。同时也可以利用这点将一些操作放在这里以达到线程同步的效果。

扩展:在单例模式懒汉式进阶版——延迟初始化占位类模式正是用了这个思想。类加载绝对是线程安全的。

image.png

 

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/109760237