Java类加载过程、ClassLoader和Class.forName详解

在这里插入图片描述

加载阶段

一个Java文件被编译成字节码文件,然后由类加载器读取此类的二进制字节流加载进JVM(Java虚拟机),并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例。程序中可以使用.class的形式获取加载阶段中的Class对象。

链接阶段

链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。

验证

验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
格式验证:验证是否符合class文件规范;
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类重写;确保父类和子类之间没有不兼容的一些方法声明;
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值,由于还没有产生对象,实例变量不在此操作范围内。被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值。这也是直接获取类中静态常量不会初始化类的原因(只能是基本类型和字符串字面量)。

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static final修饰的变量才可以使用这项属性。非static类型的变量的赋值是在实例构造器方法中进行的;static类型变量赋值分两种,在类构造器中赋值(不是实例构造器),或使用ConstantValue属性赋值。只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String。编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值,如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在类构造器中进行初始化。
public static int value = 123;//变量在准备阶段后的初始值为0而不是123,因为这时候尚未开始执行任何java方法
public static final int value = 123;//准备阶段虚拟机就会根据ConstantValue属性的设置将value赋值为123

解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)

加载与链接阶段ClassLoader类

ClassLoader:
defineClass():用于加载二进制文件到jvm,转换为一个类的class对象实例(这就是加载阶段),并没有到链接阶段,如果希望类被链接可以调用resolveClass方法。要用这个Class时,一定要执行resolveClass()。
resolveClass(Class<?> c):表示执行链接阶段。

测试:

package classloader;

public class Entity {
    public static Init i = new Init();
}
class Init {
    public Init(){
        System.out.println("init()");
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class clazz = classLoader.loadClass("classloader.Entity");
        System.out.println(clazz);
    }
}
//output:
class classloader.Entity

看到loadClass方法并没有执行类的初始化。

再来看loadClass的源码:

//默认不执行链接阶段
public Class<?> loadClass(String name) throws ClassNotFoundException {
	return loadClass(name, false);
}
    
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //首先检查要加载的这个类是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//双亲委派机制,先让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);//执行父类的加载
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);//需要继承ClassLoader自己实现

                    // 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;
        }
    }

我画了张图阐述了下流程:
在这里插入图片描述

双亲委派机制:当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类加载器都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
使用双亲委托机制的好处是能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。

JVM不能用同一个类加载器对象加载同一个class文件两次,否则会报错
自定义文件加载器:

/**
 * JVM不能用同一个类加载器对象加载一个class文件两次
 */
public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
        	//加载数据到JVM,并生成对应的Class对象(加载阶段)
        	//同一个类加载器对象不能加载同一个文件两次,否则将抛异常:java.lang.LinkageError: loader (instance of  classloader/FileClassLoader): attempted  duplicate class definition for name: "classloader/User"
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 获取class文件并转换为字节数组
     *
     * @param name
     * @return
     */
    private byte[] getClassData(String name) {
        String path = classNameToPath(name);
        InputStream ins = null;
        ByteArrayOutputStream baos = null;
        try {
            ins = new FileInputStream(path);
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int bytesNumRead = -1;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (ins != null) {
                try {
                    ins.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 类文件的完全路径
     *
     * @param name
     * @return
     */
    private String classNameToPath(String name) {
        return rootDir + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) {
        String rootDir = "C:/Users/ljh/Desktop/";
        FileClassLoader loader = new FileClassLoader(rootDir);
        try {
            Class obj1 = loader.loadClass("classloader.User");
            Class obj2 = loader.loadClass("classloader.User");
            System.out.println(obj1 == obj2);//true,确实取的是加载过的对象
            Thread.currentThread().setContextClassLoader(loader);
            Class obj3 = Thread.currentThread().getContextClassLoader().loadClass("classloader.User");
            System.out.println(obj1 == obj3);//true
            Class obj4 = new FileClassLoader(rootDir).loadClass("classloader.User");
            System.out.println(obj1 == obj4);//false,不同的类加载器对象即使加载的是同一个类的二进制文件,产生的类对象也是不同的
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
//output:
true
true
false

为什么同一个ClassLoader对象loadClass同一个class文件多少次,产生的都是同一个class对象呢?
:因为加载类前会先查找该类是否已经被加载,如果已经被加载就直接返回,否则才会执行加载逻辑。

初始化阶段

将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是方法,即类/接口初始化方法。该方法的作用就是初始化一个类中的变量,用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用方法,因为该方法只能在类加载的过程中由JVM调用。
如果父类还没有被初始化,那么优先对父类初始化,但在方法内部不会显示调用父类的方法,由JVM负责保证一个类的方法执行之前,它的父类方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。

Class.forName方法都是已经执行完了链接阶段
Class.forName(String name):默认的initialize为true,就是执行初始化阶段。

public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

Class.forName(String name, boolean initialize, ClassLoader loader):initialize参数可以设置是否执行初始化阶段

如果name表示的是一个数组类型,例如Object[],那么数组的实际包含类(Object)会被加载,但不会被初始化
无法获取基本类型和Void类型的Class对象

测试:

package classloader;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("classloader.Entity", true, Entity.class.getClassLoader());
    }
}
//output:
init()

说明执行了类加载的初始化阶段。

package com;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("classloader.Entity", false, Entity.class.getClassLoader());
    }
}
//output:

说明未执行类加载的初始化阶段。本来想用反射验证此时处在链接结束,初始化未开始的阶段,就是i = null;但是失败了,查了下发现反射调用一个类的时候,会自动初始化这个类,就导致输出了init()。

https://www.cnblogs.com/xiaoxian1369/p/5498817.html
https://blog.csdn.net/honjane/article/details/51835636

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值