JVM类加载机制学习

类的生命周期

加载——验证——准备——解析——初始化——使用——卸载

加载——————链接——————初始化——使用——卸载

                (验证、准备、解析统称链接)

什么情况会导致类的加载

1.遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化

生成这 4 条指令的常见 Java 代码场景:
new:使用 new 关键字实例化对象
getstatic:读取一个类的静态字段
putstatic:设置一个类的静态字段
invokestatic:调用类的静态方法

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

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

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

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

接下来说说类加载机制的流程:

1.加载  

        内存中会生成这个类的java.lang.Class对象来描述这个类,手段有多种,可以从zip、jar中获取,或是自己计算生成,或是从其他文件生成。

2.验证

        JVM会验证这个Class对象是否满足JVM的要求。

3.准备

        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段(不同于代码里面的初始化),这些变量所使用的内存都将在方法区中分配。

    如:public static int var1=8080;在准备阶段,var1的初始值是0,在初始化阶段,var1才会被赋值为8080。

           var1的初始化被放在<clinit>方法中,若var1是final类型的,它的初始值就不赋值 了,到时候<clinit>会从<constrantValue>中取出它的值再赋值给它。(final类型的变量只允许赋值一次)

注意

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。假设一个类变量的定义为:public static int value = 3,那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

4.解析

     将常量池的符号引用替换为直接引用。

5.初始化

     执行JAVA代码,初始化这个类。或者说初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法的执行规则:

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  • <clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • <clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是接口鱼类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

类加载模型

       JVM通过双亲委派模型进行类加载,当一个加载器收到某个类的加载请求,会先把请求移交给父类,让父类来加载,父类搞不定,自己再尝试加载这个类。

类加载器

启动类加载器     BootSrap ClassLoader    加载JAVA_HOME\lib  里面的类

扩展类加载器     Extension ClassLoader    加载JAVA_HOME\lib\ext  里面的类

应用程序类加载器     BootSrap ClassLoader    加载ClassPath 里面的类

自定义加载器             User ClassLoader

不会初始化的几种情况

1.子类引用父类的静态字段,只会触发父类初始化,不会触发子类初始化。

2.定义对象数组,不会触发该类的初始化。

3.常量在编译期间会存入调用类的常量池中,本质上并没有引用定义常量的类,不会触发定义常量所在的类。

4.通过Class.forName的反射方法加载指定的类时,不会触发类的初始化。

5.通过ClassLoader默认的loadClass方法。

后面看到什么再补充吧!

猜你喜欢

转载自blog.csdn.net/weixin_38785199/article/details/82966418