类加载Java虚拟机中类加载的过程就是将Class类文件加载到内存中,并对Class文件中的数据进行校验、转换、解析、初始化,最终形成一个Java类型的过程可以直接被虚拟机使用的语言不同于那些需要在编译时链接的语言。在Java语言中,类型加载、链接和初始化过程都是在程序执行过程中完成的。类加载时性能开销略有增加,但会为java应用提供比较高的灵活性。当我们使用某个类的时候,如果这个类还没有从磁盘加载到内存中,那么JVM会通过三步策略(加载、连接、初始化)对这个类进行初始化,而JVM完成这三步的命名该步骤称为类加载或类初始化和类加载的时间。什么情况下需要开始类加载的第一阶段——加载。JavaVirtualMachineSpecification中并没有强制约束,而是交给了虚拟机。但是,对于初始化阶段,虚拟机规范严格规定了类必须立即初始化的情况有五种(加载、验证、准备自然需要在此之前开始),具体情况如下:类文件的加载时机:序号详解1:当使用new关键字实例化一个对象,读取类的静态变量时(final修饰的静态字段除外,已经将结果放入常量池在编译时)set作为类的静态变量调用类的静态方法时注意:newarray指令只触发数组类型本身的初始化,不会引起其相关类型的初始化。比如newString[]只会直接触发String[]类的初始化,后者会触发类[Ljava.lang.String的初始化,而不会直接触发String类的初始化。生成这四种指令最常见的Java代码场景是:对于这5种会触发类初始化的场景,虚拟机规范中使用了一个非常强的限定符:“yesandonly”,这5种场景中的行为称为对类的主动引用。除此之外,所有不触发初始化的引用类的方式都称为被动引用。需要指出的是,类实例化和类初始化是两个完全不同的概念:类实例化是指创建一个类的实例(对象)的过程;类初始化是指给类的每个成员赋初值。价值过程是类生命周期中的一个阶段;被动引用的三种场景:通过子类引用父类的静态字段不会导致子类初始化/***@program:jvm*@ClassNameTest1*@Description:通过子类引用父类的静态字段subclass不会导致子类初始化*@author:猫小农*@create:2021-02-2711:42*@Version1.0**/publicclassTest1{static{System.out.println("InitSuperclass!!!");}publicstaticvoidmain(String[]args){intx=Son.count;}}classFatherextendsTest1{staticintcount=1;static{System.out.println("Initfather!!!");}}classSonextendsFather{static{System.out.println("Initson!!!");}}输出:InitSuperclass!!!Initfather!!!对于静态字段,只有直接定义该字段的类才会被初始化,所以通过其子类去引用父类中定义的静态字段只会触发父类的初始化,不会触发子类的初始化。至于是否触发子类的加载和校验,在虚拟机中并没有明确规定,要看虚拟机的具体实现。对于SunHotSpot虚拟机,可以通过-XX:+TraceClassLoading参数观察到该操作会导致子类加载。在上面的例子中,由于在Father类中定义了count字段,因此该类将被初始化。另外,类Father在初始化的时候,虚拟机发现它的父类Test1还没有被初始化,所以虚拟机会先初始化它的父类Test1,然后再初始化子类Father,而Son永远不会被初始化;通过数组定义引用类不会触发这个类的初始化/***@program:jvm*@ClassNameTest2*@description:*@author:muxiaonong*@create:2021-02-2712:03*@Version1。0**/publicclassTest2{publicstaticvoidmain(String[]args){M[]m=newM[8];}}classM{static{System.out.println("InitM!!!");}}运行后,我们会发现没有输出“InitM!!!”,说明会存储初始化阶段不触发类的常量在编译阶段在调用类的常量池中。本质上并没有直接引用定义常量的类,所以不会触发定义常量的类的初始化/***@program:jvm*@ClassNameTest3*@description:*@author:muxiaoonong*@create:2021-02-2712:05*@Version1.0**/publicclassTest3{publicstaticvoidmain(String[]args){System.out.println(ConstClass.COUNT);}}classConstClass{staticfinalintCOUNT=1;static{System.out.println("InitConstClass!!!");}}上面的代码并没有输出InitConstClass!!!,这是因为Java源码中虽然引用了ConstClass类中的常量COUNT,但实际上常量在编译阶段传播和优化。Test3常量池中存放的是值“1”,对常量ConstClass.COUNT的引用实际上转换为Test3类对自身常量池的引用,也就是实际上是Test3的Clas文件中没有ConstClass类的符号引用入口,这两个类编译成Class文件后没有任何关系。类加载过程中有一个Class文件,静静地躺在硬盘上,很受欢迎。热血沸腾,他需要经历怎样的过程才能从舒服的硬盘到内存?类进入内存的三个步骤:加载(Loading)、链接(Linking)、初始化(Initlalizing)1.加载和加载是类加载(ClassLoading)过程的一个阶段。加载是类加载(ClassLoading)过程的一个阶段。加载是指将当前类的class文件读入内存,创建java.lang.Class对象。也就是说,当程序中使用到任何一个类时,系统都会创建一个名为java的对象。二进制字节流(不指定只能从一个Class文件中获取,可以从其他渠道获取,如:网络、动态生成、数据库等)将这个字节流表示的静态存储结构转换进入方法区运行时数据结构体在内存中生成一个代表该类的java.lang.Class对象,作为该类各种数据在方法区的访问入口。类加载器通常不需要等到类“第一次使用”时才加载类。Java虚拟机规范允许系统预加载某些类。加载阶段和连接阶段的部分内容是交错的。加载阶段还没有完成,连接阶段可能已经开始,但是夹紧阶段执行的这些动作仍然属于连接阶段的内容。这两个阶段的开始时间仍然保持着固定的顺序。2.连接类加载完成后,系统会生成相应的Class对象,进入连接阶段。连接阶段负责将类的二进制数据合并到JRE中。连接阶段分为三个子阶段1.1VerificationVerification是连接阶段的第一步,这个阶段的主要目的是确保Class文件的字节流中包含的信息满足当前虚拟机的要求机,不会危及虚拟机本身的安全。与C/C++相比,Java语言本身是一种相对安全的语言。验证阶段非常重要。这个阶段是否严谨决定了Java虚拟机能否抵御恶意代码的攻击。当验证输入字节流不符合Class文件格式约束时,虚拟机抛出java.lang.VerifyError异常或子类异常。一般来说,验证主要分为四种验证动作:文件格式验证、元数据验证、字节码验证、Symbol引用验证文件格式验证:主要验证字节流是否符合Class文件格式的规范,是否可以处理通过当前版本的虚拟机。主要包括以下几个方面:文件格式是否以CAFEBABE开头主次版本是否在虚拟机处理范围内常量池中的常量是否有不支持的常量类型指向常量的各种索引值是否指向对不存在的常量或者不符合常量类型CONSTANTUtf8infoClass文件的各个部分是否有不符合UTF8编码的数据,文件本身是否有被删除的活附件信息。元数据校验:主要是对字节码描述的信息进行语义分析,主要目的是对类的元数据进行语义校验,分析是否符合Java语言语法规范,确保没有不符合Java语言规范的元数据信息。这个阶段的验证主要有以下几个方面:这个类是否有父类(java除外。接口中需要的所有方法类中的字段和方法是否与父类冲突字节码验证:最重要也是最重要的复杂的验证环节是通过数据流和控制流来分析程序语义是否合法和合乎逻辑,主要是对类的方法体进行检查和分析,确保被检查的类在运行时不会危及虚拟机的安全。操作数栈的数据类型和指令码序列在任何时候都可以协同工作(比如操作栈中有一个int类型的数据,保证不会根据加载到局部变量表中)使用时为long类型)跳转指令不会剥离为方法体以外的字节码指令,保证方法体中的数据转换是va盖。例如,子类对象可以赋给父类数据类型,但父类不能赋给子类数据类型。符号引用验证:当将直接引用转换为符号引用时,此转换工作将在解析阶段发生的第三阶段(字节码验证)完成。主要是保证引用会被访问到,不会出现类不能访问的问题。1.2准备为类变量分配内存,设置类变量初值。这些变量使用的内存将分配在方法区中。在准备阶段,类文件的静态变量被赋予默认值。注意:不要赋初值,比如我们的publicstaticinti=8,在这一步中,不是给i赋8,而是先给基本类型赋默认值0:一般情况下,初值为0,但是如果我们给上面的常量加上一个final类,那么这个时候就会编程初始值。我们指定的值publicstaticfinalinti=8javac在编译的时候会把i的初始值改成8。无障碍内容符号引用:一组符号用于描述引用的目标。该符号可以是任何文字形式。只要不冲突,可以定位,就可以直接引用:可以是直接指向目标的指针,Relativeoffset,也可以是可以间接定位到目标的句柄。如果存在直接引用,则引用的目标必须已经存在于内存中。3.初始化初始化就是给类的静态变量赋予正确的初始值。刚才我们提到准备阶段是复制默认值,初始化是给静态变量赋初值,看下面的语句:publicstaticinti=8首先,字节码文件加载后入内存,先进行连接验证,通过准备阶段,分配内存给i,因为是静态的,所以此时i等于int类型默认初始值为0,所以i现在0,而在初始化的时候,实际上会将i赋值给8个类加载器,类加载器负责加载所有的类,并在内存中为加载的类生成一个java.lang.Class实例对象。如果一个类被加载到JVM中,同一个类就不会被再次加载,就像对象有一个唯一的标识符一样,同一个被加载的JVM类也有一个唯一的标识符。JVM本身具有类加载器的层次结构。这个类加载器本身就是一个普通的Class。所有类都由类加载器加载到内存中。我们可以称它为ClassLoader,一个顶层父类,一个abstract抽象类。Bootstrap:类加载器的加载过程,分为不同的层级进行加载。不同的类加载器加载不同的类。作为顶层的Bootstrap,加载了lib中JDK的核心内容,如rt.jarcharset。jar等核心类,当我们调用getClassLoader()获取加载器,结果为Null时,说明我们到达了顶层加载器Extension:扩展加载器扩展类,加载扩展包中的各种类型这些扩展包在JDK安装目录jre/lib/ext下的jarApp中:就是我们平时用来加载classpath指定内容的应用CustomClassLoader:自定义ClassLoader,加载自己的自定义加载器CustomClassLoader父类加载器是application的父类加载器是Extension父类加载器是Bootstrap注意:不是继承关系,而是委托关系因为Bootstrap是用C++实现的//Java中没有类及其对应的System.out.println(String.class.getClassLoader());//这是在一个包里e核心类库类被执行,执行结果为Null,因为这个类也是被Bootstrap加载的System.out.println(sun.awt.HKSCS.class.getClassLoader());//这个类位于ext目录下的一个jar文件,我们调用他的时候,执行结果是sun.misc.Launcher$ExtClassLoader@a09ee92System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//这个是我们自己写device的ClassLoad加载,通过sun.misc.Launcher$AppClassLoader@18b4aac2System.out.println(ClassLoaderTest.class.getClassLoader());//Exe的ClassLoader调用它的getclass(),它本身就是一个类,调用它的getClassLoader,它的ClassLoader的ClassLoader就是我们的Bootstrap,所以结果是NullSystem.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getClass().getClassLoader());}}类加载器继承关系该图从语法上展示了类加载器继承自谁。这个图只是简单的语法关系,不是继承关系。你可以记住它与上面的类加载无关。如果你太多了,你其实可以忽略这个图。loader”,也不是“类加载器的父类加载器”。尽量从自定义中寻找。同时,它内部也维护着缓存,如果在缓存中找到,则直接返回结果。如果没有找到,就委托给父类,由父类去缓存中查找,直到到达顶层,如果此时还没有从缓存中获取到我们想要的结果,则父亲会告诉我你的情况,我做不到,你得自己做,然后儿子自己去查询对应的类,然后Loading,如果小儿子还是没有找到对应的类,就抛出一个exceptionClassNotFoundException父母为什么要delegate,这个是类加载器必问的面试题,主要是为了安全,如果有一个Class是可以加载到内存中的,那我写一个java.lang.String.如果我写了危险代码,会不会有安全问题,可以保证tyJava核心api中定义的pes不会被随意替换,可以防止API库被随意更改,其次是效率问题。如果有缓存,直接从缓存中取就可以了,就不用我们的父类或者子类一遍又一遍的遍历查询了。
