类加载机制主要分3块:类加载器(ClassLoader)、类加载过程和双亲委托(破坏双亲委托)
类加载器:是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。jdk自带了三种类加载器,分别是引导类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader)。后两种加载器是继承自抽象类java.lang.ClassLoader
类加载过程:类加载过程包括加载、验证、准备、解析和初始化五个阶段。
1、加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对戏那个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
2、链接
链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
1)、验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法视频被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)
2)、准备
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
3)、解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
3、初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
如果父类还没有被初始化,那么优先对父类初始化,但在<clinit>方法内部不会显示调用父类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的父类<clinit>方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
双亲委托(破坏双亲委托):
程序运行后,编译器把Java文件编译成class文件后,首先负责加载的是系统类加载器,但它不会马上加载,而是将此任务移送给它的父类加载器扩展类加载器加载,扩展类加载器也是将此任务移送给引导类加载器加载。
class文件到了引导类加载器那,它先判断能不能加载这个类,如果能,就加载;不能,移送给其子加载器,以此类推。最终我们编写的class都会配置在classpath环境中,所以,这个类加载任务还是由系统类加载器完成。如果系统类加载器都不能加载,就抛出ClassNotFoundException。
当一个class加载到JVM中,类加载阶段已经完成。接下来JVM分配内存,对整个class文件(文件里面都是二进制的汇编命令)进行内容解析(JVM对二进制的命令逐行解析,交由CPU执行)。
双亲委托机制:这种机制的好处就是不会随随便便的加载用户写的类。
双亲委托在jdk8中的实现,ClassLoader中的loadClass方法
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); // 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得 } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null) { // 如果还是没有获得该类,调用findClass找到类 long t1 = System.nanoTime(); c = findClass(name); // jvm统计 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方法即可,反之,重写findClass方法。这样保证了不会随随便便的加载用户写的类
---------------------------------------------------------------------------------------------------------------
附:是否可以写一个类叫"java.lang.String"
不能写这个类,我们自定义的类加载器必须继承自ClassLoader,其loadClass()方法里调用了父类的defineClass()方法,并最终调到preDefineClass()方法,因此我们自定义的类加载器也是不能加载以“java.”开头的java类的。
参考资料:
https://www.cnblogs.com/joemsu/p/9310226.html
https://blog.csdn.net/tang9140/article/details/42738433
https://www.cnblogs.com/xiaoxian1369/p/5498817.html
原文地址:https://www.cnblogs.com/AyasatoMayoi/p/9966840.html