Java虚拟机四:类加载机制

Posted 刘镓旗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机四:类加载机制相关的知识,希望对你有一定的参考价值。

虚拟机把描述Class文件加载到内存,并对数据进行校检、转换解析、初始化,最终形成可以被虚拟机使用的Java类型,将这个过程称之为类的加载机制。

一、类的加载过程

类从被加载到内存开始,会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、和卸载(Unloading)这7个阶段,其中验证、准备和解析3个统称为连接(Linking)。

其中加载、验证、准备、初始化和卸载这5个接口的顺序是确定的,但是解析阶段是不一定的,因为java语言的动态绑定。

1。加载(Loading) :

加载阶段虚拟机要完成3件事情:

1。通过一个类的全限定名来获取定义此类的二进制流。

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

3。在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

类的加载时机
Java虚拟机规范中并没有强制约束什么时候开始进行加载。但是对于初始化阶段,虚拟机规范严格规定了“有且只有5种情况必须立即对类进行初始化”,而加载、验证、准备肯定是在初始化之前要做的,那其实也间接的约束加载,也可以理解为类的加载时机。

1。遇到new、getstatic、pubstatic或invokestatic这4条字节码指定时,如果类没有进行初始化,需要先初始化。这4个指令对应Java代码的场景分别是:使用new关键字创建对象时、读取或者设置一个类的静态自动(不被final修饰的)、调用一个类的静态方法时。

2。使用java.lang.reflect包中的方法对类进行反射调用时,如果没有初始化先初始化。

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

4。当虚拟机启动时,用户指定一个要执行的主类,就是程序的入口,带main方法的类,那么要先初始化这个主类。

5。当使用JDK1.7的java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这和对应的类有没初始化,要先触发初始化。

上边5中叫做主动引用,除了这5种情况其他引用类的情况不会被初始化,也叫做被动引用。

被动引用例子1:

/**
*被动使用类字段演示一:
*通过子类引用父类的静态字段,不会导致子类初始化
**/
public class SuperClass
    static
        System.out.println("SuperClass init!");
    

    public static int value=123/**
*子类
**/
public class SubClass extends SuperClass
    static
        System.out.println("SubClass init!");
    


/**
*非主动使用类字段演示
**/
public class NotInitialization
    public static void main(String[]args)
        System.out.println(SubClass.value);
    

上面的代码只会输出“SuperClass init !”,对于静态字段,有只直接定义这个字段的类才会被初始化,通过子类直接引用父类中定义的静态字段,子类不会被初始化

被动引用例子2:

/**
*被动使用类字段演示二:
*通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization
    public static void main(String[]args)
        SuperClass[]sca=new SuperClass[10];
    

上面使用类来创建一个数组类不会被初始化

被动引用例子3:

/**
*被动使用类字段演示三:
*常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
**/
public class ConstClass
    static
        System.out.println("ConstClass init!");
    

    public static final String HELLOWORLD="hello world"/**
*非主动使用类字段演示
**/
public class NotInitialization
    public static void main(String[]args)
        System.out.println(ConstClass.HELLOWORLD);
    

上面代码只会输出“hello world”,不会输出 “ConstClass init!”,因为被final修饰的static变量在编译期间存在了ConstClass 类的常量池中,那么也就变成了ConstClass 对自己常量池的引用。

2。验证(Verification):

验证是连接阶段的第一步,这一阶段的目的为了确保Class文件字节流中包含的信息符合虚拟机的要求。验证阶段大致会从一下4个方面验证。

1。文件格式的验证:验证字节流是否符合Class文件的格式规范,例如是否是以魔数0xCAFEBABE开头的、版本号是否在虚拟机的处理范围内、常量池中是否有不支持的常量类型、CONSTANT_Uff8_info类型的常量中是否有不和UTF8编码的、Class文件中各个部分及文件本身是否有删除或者附加的信息等。

2。元数据验证:对字节码进行分析,看是否符合Java的语言规范要求。例如是否所有的类都继承自Object、这个类是否继承了被final修饰的类、如果是抽象类是否实现了父类的方法等。

3。字节码验证:通过分析字节码判断代码是合法的、符合逻辑的。例如保证跳转指针不会跳转到方法体外的指令上、保证方法体重的类型转换是有效的等。一个类的方法字节码没有通过字节码验证肯定是有问题的,但是一个方法字节码通过了验证,也不能说他一定是安全的。

4。符号引用验证:这一步是为了解析做准备,判断符号引用通过全限定名是否能找到对应的类、符号引用中的类,字段,方法的访问修饰符是否可以被当前类方法,在这一步如果如果不能通过符号引用可能会抛出如下异常:
java.lang.IllegalAccessError
java.lang.NoSuchFieldError
java.lang.NoSuchMethodError

3。准备 :

准备阶段是给static修饰的变量分配内存并设置初始值的过程。这里说了是被static修饰的变量,类中的成员变量是在对象实例化的时候随着对象分配在Java堆中的。
这里设置初始值有两种情况,如果这个static的变量没有被final修饰的话那么设置的就是这个类型的默认值,直到类执行到初始化阶段才会按照代码中设定的值初始化。
如果是被final修饰的static变量会在这里阶段被设置成代码中的值,想想为什么

4。解析 :

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,这里包括对类或者接口的解析、字段解析、类方法解析、接口方法解析。

5。初始化 :

到了初始化才真正开始执行Java代码或者说是字节码,在准备阶段变量被设置了一次初始值,在这个阶段则需要根据代码中设定的值初始化static变量和其他的资源,也可以理解为初始化阶段是执行构造器< clinit >()方法的过程。
< clinit >()方法:是由编译器自动生成的,它用来执行类中所有的static变量的赋值工作和静态语句块中的代码。执行的顺序是由Java代码的出现顺序决定的,静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,它只可以进行赋值,但不能访问。

public class Test
    static
        i=0//给变量赋值可以正常编译通过
        System.out.print(i);//这句编译器会提示"非法向前引用"
    
    static int i=1

< clinit >()方法与构造方法(构造器< init >()方法)不同,它不需要显示的调用父类的构造器,虚拟机会保证在子类< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕,所以虚拟机中第一个被执行的< clinit >()方法肯定是Object。如果类中没有静态语句块也没有static变量的赋值,那么编译器不会生成clinit >()。

接口中不能使用静态语句块,但是也会有变量赋值的操作,因此接口也会生成clinit >()方法,但是接口与类不同的是,执行接口的clinit >()方法不需要先执行父接口的clinit >()方法。只有当父接中定义的变量使用时才会初始化。接口的实现类在初始化的时候也一样不会执行接口的clinit >()方法。

虚拟机会保证一个类的clinit >()方法在多线程中被正确的加锁、同步,如果有多个线程同时初始化一个类,那么只会有一个线程去执行这个类的clinit >(),其他的线程都会被阻塞等待,知道clinit >()执行完毕,所以尽量不要再clinit >()方法中执行耗时的操作。

二、类加载器

Java虚拟机中提供了3种类型的加载器,这里说的是3种类型,并不就是这3个类加载器,不同的虚拟机可能名字不一样,而且,同类型的也肯能不只一个类加载:

Bootstrap ClassLoader : 这个类并没有继承自ClassLoader,而且在部分虚拟机中这个类可能是用C语言实现的,因为它主要用来加载Java的核心类库,主要加载JAVA_HOME\\lib目录中的核心类库,或者被-Xbootclasspath参数所指定路径下的类库。除了这个类外,Java所有的类加载器都要继承ClassLoader

Extension ClassLoader :这个类主要负责加载Java的扩展库JAVA_HOME\\lib\\ext目录中类库,这个类由sun.misc.Launcher的内部类ExtClassLoader实现

Application ClassLoader:或者叫做SystemClassLoader,因为我们通过getSystemClassLoader()方法返回的就是这个类,这个类也是程序的默认加载器,它主要负责加载classpath指定的类库。这个类由sun.misc.Launcher的内部类AppClassLoader实现

    //ClassLoader类中的getSystemClassLoader方法
  @CallerSensitive
    public static ClassLoader getSystemClassLoader() 
        //初始化AppClassLoader
        initSystemClassLoader();
        if (scl == null) 
            return null;
        
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) 
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        
        return scl;
    
//ClassLoader类中的initSystemClassLoader方法
private static synchronized void initSystemClassLoader() 
        if (!sclSet) 
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
                //sun.misc.Launcher
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

            if (l != null) 
                Throwable oops = null;
                //获取AppClassLoader
                scl = l.getClassLoader();
                ....

            sclSet = true;
        


    //上面sun.misc.Launcher调用的getClassLoader()
   public ClassLoader getClassLoader() 
       return this.loader;
   
    //sun.misc.Launcher的构造方法
    public Launcher() 
            Launcher.ExtClassLoader var1;
            try 
                //ExtClassLoader 
                var1 = Launcher.ExtClassLoader.getExtClassLoader();
             catch (IOException var10) 
                throw new InternalError("Could not create extension class loader", var10);
            

            try 
                //AppClassLoader
                this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
             catch (IOException var9) 
                throw new InternalError("Could not create application class loader", var9);
        

        Thread.currentThread().setContextClassLoader(this.loader);

      ....

    

双亲委派模型:
同一个Class文件如果使用不同的类加载器加载,那么这个类就不是同一个类,为了保证一个类的唯一性,Java采用了双亲委派模型,如果一个加载器要加载一个类的时候,它并不会自己去加载,而是首先会向上委托给父类去加载,层层向上,直到最顶层的类加载器,如果父类不能加载成功,子类才会尝试自己去加载。

//ClassLoader的双亲委派模型
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    
        synchronized (getClassLoadingLock(name)) 
            // 查找这个类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) 
                long t0 = System.nanoTime();
                try 
                    if (parent != null) 
                        //未加载先委托父类加载
                        c = parent.loadClass(name, false);
                     else 
                        //如果父类为空则使用Bootstrap ClassLoader
                        c = findBootstrapClassOrNull(name);
                    
                 catch (ClassNotFoundException e) 
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                

                if (c == null) 

                    long t1 = System.nanoTime();
                    //如果父类还未找到尝试自己加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                
            
            if (resolve) 
                resolveClass(c);
            
            return c;
        
    

以上是关于Java虚拟机四:类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

JVM类加载机制概述:加载时机与加载过程

虚拟机的类加载机制

虚拟机类加载机制

深入理解JVM(③)虚拟机的类加载时机

类加载的时机和过程

Java 虚拟机程序执行:02 虚拟机的类加载机制