本文转载自微信公众号《爱写bug的米罗》,作者米罗。转载本文请联系爱写bug的Milo公众号。一、类加载机制1、什么是类加载?熟悉java开发的同学都知道,我们每天写的代码都保存在.java文件中。这些“.java”文件被Java编译器编译成扩展名为“.class”的文件。“.class”文件存放Java代码转换后的虚拟机指令。当需要使用一个类时,虚拟机就会加载它的“.class”文件,并创建相应的类对象,并将class文件加载到虚拟机的内存中。这个过程称为类加载2。类加载的过程是加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备和分析三部分统称为连接。这七个阶段的顺序如下:加载、验证、准备、解析和初始化是类加载机制中的步骤。请注意,这里的加载与类加载不同。对比两张图就明白了。3.类加载的触发条件①。当遇到new、getstatic、putstatic或invokestatic这四个字节码指令时,如果类还没有初始化,需要先触发初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时,读取或设置类的静态字段时(被final修饰,结果在编译时已经放入常量池静态字段除外),以及调用类的静态方法时。②.当使用java.lang.reflect包的方法对类进行反射调用时。③.在初始化一个类的时候,如果发现它的父类还没有初始化,就需要先开始父类的初始化。④.虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机首先初始化主类。⑤.在使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的最终解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而这个方法句柄对应的类还没有初始化,你需要先初始化。4.类加载的具体过程Loading:①.通过类全限定名获取定义该类的二进制字节流②。将这个字节流表示的静态存储结构在方法区结构③中转换为运行时数据。在内存中生成一个代表该类的java.lang.Class对象,作为该类各种数据在方法区的访问入口。验证:是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危及虚拟机本身的安全。验证动作包括四个阶段文件格式验证验证字节流是否符合Class文件格式的规范,可以被当前版本的虚拟机处理。b.元数据校验对类的元数据信息进行语义校验,是否没有不符合Java语言规范的元数据信息c.字节码验证最复杂的阶段,主要目的是分析数据流和控制流,判断程序语义是否合法和合乎逻辑。对类的方法体进行验证分析,确保被验证类的方法在运行时不会引发危害虚拟机安全的事件。d.符号引用验证验证的最后阶段发生在虚拟机将符号引用转换为直接引用时,这个转换动作将发生在连接的第三阶段——解析阶段。符号校验的目的是保证解析动作能够正常进行。准备阶段:准备阶段是正式为类变量分配内存,并设置类变量初值的阶段。这些变量使用的内存将分配在方法区中。仅包含类变量。初始值“通常”是数据类型的零值。在“特殊情况”下,如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量的值将被初始化为ConstantValue属性指定的值。解决方法:虚拟机将常量池中的符号引用替换为直接引用。“动态解析”的意思是,要等到程序真正运行到这条指令时,才能进行解析动作。相比之下,其余能够触发解析的指令都是“静态”的,可以在加载阶段刚刚完成,代码还没有开始执行的时候解析。初始化:类加载过程的最后一步。初始化阶段就是执行类的constructor()方法的过程。()方法是由编译器自动收集的类中所有类变量的赋值动作和静态语句块中的语句组合生成的。()与类的构造函数不同,它不需要显式调用父类的构造函数,虚拟机保证在子类的()方法执行之前先执行父类的()方法。简单的说,初始化就是给类变量赋值,执行静态代码块。2.类加载器通过上面的了解,我们已经知道了类加载机制的大致流程和各部分的作用。加载部分的作用是将类的class文件读入内存,并为其创建java.lang.Class对象。这部分功能由类加载器实现。1.类加载器分类:不同的类加载器负责加载不同的类。主要有两类。BootstrapClassLoader:由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定路径下的类库加载到内存中,即负责加载Java的核心类。ExtensionClassLoader:负责加载\lib\ext目录或java.ext.dirs系统变量指定路径下的所有类库,即负责加载Java扩展核心类以外的类。应用类加载器(ApplicationClassLoader):负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,直接通过ClassLoader.getSystemClassLoader()方法获取。一般来说,如果我们没有自定义类加载器,默认使用这个加载器。其他类加载器:由Java语言实现,继承自抽象类ClassLoader。下面我们来详细了解一下上述类加载器是如何相互配合来实现类加载过程的。2.双亲委托模型双亲委托模型的工作流程是:如果一个类加载器收到一个类加载请求,它不会先尝试自己加载类,而是将请求委托给父加载器完成,然后再去up,因此,所有的类加载请求最终都应该传递给顶层启动类加载器,只有当父加载器在其搜索范围内没有找到需要的类,即无法完成加载时,子加载器才会将尝试自行加载类。这样做的好处是不同层次的类加载器有不同的优先级。例如,所有Java对象的超父类java.lang.Object位于rt.jar中。不管是哪个类加载器加载的类,最终都会被启动类加载。设备加载以确保安全。即使用户写了一个java.lang.Object类放到程序中,虽然可以正常编译,但是不会加载运行,保证不会出现混乱。3.双亲委托模型的代码实现ClassLoader中的loadClass方法实现了双亲委托模型protectedClass>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){//检查类是否已经加载Classc=findLoadedClass(name);if(c==null){//如果没有加载类,则进入分支longt0=System.nanoTime();try{if(parent!=null){//当类的加载器theparentclassisnot如果为空,通过父类的loadClass加载类c=parent.loadClass(name,false);}else{//当父类的loader为空时,调用启动类loader加载类c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){//非null父类的类加载器找不到对应的类,抛出异常}if(c==null){//当父类加载器无法加载时,再调用findClass方法加载类longt1=System.nanoTime();c=findClass(name);//用户可以通过重写该方法来自定义类加载器//用于统计类加载器相关信息sun.misc.PerfCounter.getParentDelegationTime().addTime(t1-t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if(resolve){//ResolveClass(c);}returnc;}}整个过程大致如下:首先检查指定名称的类是否已经加载。加载并直接返回。b.如果这个类没有被加载过,则判断是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name,false);)。或者调用bootstrap类加载器加载。C。如果父加载器和引导类加载器都没有找到指定的类,则调用当前类加载器的findClass方法完成类加载。
