zoukankan      html  css  js  c++  java
  • 看懂Class文件的装载流程

    Class文件的加载过程

    ClassLoader的工作模式

    类的热加载


    1 Class文件的装载流程

    只有被java虚拟机装载的Class类型才能在程序中使用(注意装载和加载的区别

    1.1 类装载的条件

     Class只有在必须要使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。这里的使用指的是主动使用,主动使用有以下几种情况:

    • 当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化。
    • 当调用类的静态方法时,即当使用了字节码invokestatic指令
    • 当使用类或者接口的静态字段时(final常量除外),即使用getstatic或者putstatic指令
    • 当使用java.lang.reflect包中的方法反射类的方法时
    • 当初始化子类时,必须先初始化父类
    • 作为启动虚拟机、含有main方法的那个类

    除了以上情况属于主动使用外,其他情况均属于被动使用,被动使用不会引起类的初始化

    例1:主动使用

    public class Parent{

      static{

        System.out.println("Parent init");

      }

    }

    public class Child{

      static{

        System.out.println("Child init");

      }

    }

    public class InitMain{

      public static void main(String[] args){

        Child c = new Child();

      }

    }

    以上声明了3个类:Parent Child InitMain,Child类为Parent类的子类。若Parent被初始化,根据代码中的static块可知,将会打印"Parent init",若Child被初始化,则会打印"Child init"。执行InitMain,结果为:

    Parent init 

    Child init

    由此可知,系统首先装载Parent类,接着装载Child类。符合主动装载中的两个条件,使用new关键字创建类的实例会装载相关的类,以及在初始化子类时,必须先初始化父类。

    例2 :被动装载

    public class Parent{

      static{

        System.out.println("Parent init ");

      }

      public static int v = 100; //静态字段

    }

    public class Child extends Parent{

      static{

        System.out.println("Child init");

      }

    }

    public class UserParent{

      public static void main(String[] args){

        System.out.println(Child.v);

      }

    }

    Parent中有静态变量v,并且在UserParent中,使用其子类Child去调用父类中的变量。

    运行代码:

    Parent init

    100

    虽然在UserParent中,直接访问了子类对象,但是Child子类并未初始化,只有Parent父类进行初始化。所以,在引用一个字段时,只有直接定义该字段的类,才会被初始化。

    注意:虽然Child类没有被初始化,但是,此时Child类已经被系统加载,只是没有进入初始化阶段。

    可以使用-XX:+ThraceClassLoading 参数运行这段代码,查看日志,便可以看到Child类确实被加载了,只是初始化没有进行

    例3 :引用final常量

    public class FinalFieldClass{

      public static final String constString = "CONST";

      static{

        System.out.println("FinalFieldClass init");

      }

    }

    public class UseFinalField{

      public static void main(String[] args){

        System.out.println(FinalFieldClass.constString);

      }

    }

    运行代码:CONST

    FinalFieldClass类没有因为其常量字段constString被引用而初始化,这是因为在Class文件生成时,final常量由于其不变性,做了适当的优化。

    分析UseFinalField类生成的Class文件,可以看到main函数的字节码为:

    在字节码偏移3的位置,通过Idc将常量池第22项入栈,在此Class文件中常量池第22项为:

    #22 = String        #23     //CONST

    #23 = UTF8         CONST

    由此可以看出,编译后的UseFinalField.class中,并没有引用FinalFieldClass类,而是将其final常量直接存放在常量池中,因此,FinalFiledClass类自然不会被加载。(javac在编译时,将常量直接植入目标类,不再使用被引用类)通过捕获类加载日志(部分日志)可以看出:

    注意:并不是在代码中出现的类,就一定会被加载或者初始化,如果不符合主动使用的条件,类就不会被初始化。


    1.2 类装载的整个过程

    1)加载类

    加载类处于类装载的第一个阶段。

    加载类时,JVM必须完成:

    • 通过类的全名,获取类的二进制数据流
    • 解析类的二进制数据流为方法区内的数据结构
    • 创建java.lang.Class类的实例,表示该类型

    2)连接

     1 验证类:

    当类被加载到系统后,就开始连接操作,验证是连接的第一步。

    主要目的是保证加载的字节码是符合规范的。验证的步骤如图:

    2 准备

    当一个类验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。

    java虚拟机为各种类型变量默认的初始值如表:

    类型 默认初始值
    int 0
    long 0L
    short (short)0
    char u0000
    boolean false
    reference null
    float 0f
    double 0f

     注意:java并不支持boolean类型,对于boolean类型,内部实现是Int,由于int的默认值是0,故对应的,boolean的默认值是false

    如果类属于常量字段,那么常量字段也会在准备阶段被附上正确的值,这个赋值属于java虚拟机的行为,属于变量的初始化。事实上,在准备阶段,不会有任何java代码被执行。

    3 解析类

    在准备阶段完成后,就进入了解析阶段。

    解析阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用。

    符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行大量的符号引用。

    具体可以使用JclassLib软件查看Class文件的结构:::

    3)初始化

    初始化时类装载的最后一个阶段。如果前面的步骤没有出现问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。

    初始化阶段的重要工作是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。

     例如:

    public class SimpleStatic{

      public static int id = 1;

      public static int number;

      static{

        number = 4;

      }

    }

    java编译器为这段代码生成如下的<clinit>:

    0 iconst_1
    1 putstatic #2 <Demo.id>
    4 iconst_4
    5 putstatic #3 <Demo.number>
    8 return

    可以看出,生成的<clinit>函数中,整合了SimpleStatic类中的static赋值语句以及static语句块,先后对id和number两个成员变量进行赋值

    由于在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类<clinit>之前被调用。也就是说,子类的static块优先级高于父类。

    public class ChildStatic extends Demo{
      static{
        number = 2;
      }
      public static void main(String[] args){
        System.out.println(number);
      }
    }

    运行可知:

    2

    说明父类的<clinit>总是在子类<clinit>之前被调用。

    注意:java编译器并不是为所有的类都产生<clinit>初始化函数,如果一个类既没有赋值语句,也没有static语句块,那么生成的<clinit>函数就应该为空,因此,编译器就不会为该类插入<clinit>函数

    例如:

    public class StaticFinalClass{

      public static final int i=1;

      public static final int j=2;

    }

    由于StaticFinalClass只有final常量,而final常量在准备阶段初始化,而不在初始化阶段处理,因此对于StaticFinalClass类来说,<clinit>就无事可做,因此,在产生的class文件中没有该函数存在。

  • 相关阅读:
    CSS性能让JavaScript变慢?
    Cordova优缺点与环境部署
    nodeapi
    git常用命令
    常见状态码
    关于拉萨
    英语学习
    SQL 按表中的一个int值拆分成对应的个数的记录条数
    SQL分组编号
    C#四舍五入
  • 原文地址:https://www.cnblogs.com/zwbg/p/6187205.html
Copyright © 2011-2022 走看看