类加载
-
类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程
-
类加载的时机:加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,必须按这个顺序来,而解析阶段不一定,可以在初始化之后开始。
以下6中情况需要立刻初始化:1.使用new关键字实例化对象的时候、读取或设置一个类型的静态字段、调用一个类型的静态方法的时候, 即遇到new、getstatic、putstatic或invokestatic这四条字节码指令。 2.使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。 3.当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类 5.当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 6.当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、 REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
-
类加载的过程:
1. 加载。加载阶段虚拟机需要做的三件事: 1. 通过一个类的全限定名来获取定义此类的二进制字节流。 2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。 如果是非数组类型的加载阶段。可以通过自定义的类加载器或者虚拟机内置的类加载器来加载。 如果是数组类型,数组类本身不通过类加载器创建,它是由Java虚拟机直接在 内存中动态构造出来的。 但是数组中的元素类型如果是引用类型,那么还是可用自定义或者内置加载器。(一个类型必须与类加载器一起确定唯一性) 如果不是引用类型,把数组 标记为与引导类加载器关联。 2. 验证。确保Class文件的字节流中包含的信息符合规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 1. 文件格式验证。验证字节流是否符合Class文件格式的规范,能否被虚拟机处理。 比如:是否以魔数0xCAFEBABE开头。 主、次版本号是否在当前Java虚拟机接受范围之内。 常量池的常量中是否有不被支持的常量类型 2. 元数据验证。对类的元数据信息进行验证,保证描述的信息符合规范。验证比如:这个类是否有父类、这个类的父类是否继承了不允许被继承的类(被final修饰的类)。 3. 字节码验证。通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。本阶段属于对类的方法体进行校验分析, 保证被校验类的方法在运行时不会做出危害 虚拟机安全的行为。 4. 符号引用验证。发生在虚拟机将符号引用转化为直接引用的时候,对类自身以外(常量池中的各种符号 引用)的各类信息进行匹配性校验, 即该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源。 目的:确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机 将会抛出异常。 3. 准备。为类中静态变量分配内存并设置类变量初 始值的阶段。这时候进行内存分配的 仅包括类变量,而不包括实例变量。 例如:public static int a=100;则这个阶段a=0; 4. 解析。将常量池内的符号引用替换为直接引用。 什么是符号引用?:符号引用以一组符号来描述所引用的目标,符号可以是任何 形式的字面量,只要使用时能无歧义地定位到目标即可。 什么是直接引用?:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。 !!虚拟机对同一个符号引用可以进行多次解析,除了invokedynamic指令,虚拟机实现可 以对第一次解析的结果进行缓存,从而避免解析动作重复进行。 如果符号引用之前已经被成功解析,那么后续的引用解析请求就应当一直能够成功; 如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常,哪 怕这个请求的符号在后来已成功加载进Java虚拟机内存之中。 invokedynamic指令用来支持动态语言,必须等到程序实际执行这条指令解析动作才能成功。 5. 初始化。在准备阶段,变量已经赋过一次系统要求的初始零值,初始化阶段需要将这些变量赋值为程序员设定的内容。 即初始化阶段就是执行类构造器<clinit>()方法的过程。 需要注意的几个点:1.<clinit>()方法与类的构造函数(即实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行 完毕。 2.由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作 3.虽然接口中不能使用静态语句块,但仍然会生成<clinit>()方法。 4.如果多个线程同时初始化一个类,则只有一个线程去执行<clinit>()方法。
-
类加载器
1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib ⽬录下的jar包和类或者或 被 -Xbootclasspath 参数指定的路径中的所有类 2. ExtensionClassLoader(扩展类加载器) :主要负责加载⽬录 %JRE_HOME%/lib/ext ⽬录下的jar包和类,或被 java.ext.dirs 系统 变量所指定的路径下的jar包。 3. AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。 4. 我们也可以自定义类加载器,继承ClassLoader,重载loadClass() 。
-
双亲委派模型
1. 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承的关系来实现的, 而是通常使用 组合关系来复用父加载器的代码。 2. 双亲委派模型工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加 载这个类,而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求 (它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 3. 双亲委派模型的优点: 1. Java类随着它的类加载器一起,具备了带有优先级的层次关系。 2. 避免类的重复加载,保证一种类都由同一个类加载器加载。不会出现多个不同的Object类。
-
破坏双亲委派模型
https://www.cnblogs.com/fengtingxin/p/11872710.html
双亲委派模型存在的问题:父加载器无法向下识别子加载器的资源。
如何解决?:引入了线程上下文类加载器(Thread Context ClassLoader),可以通过类的setContextClassLoader()方法
进行设置。再通过getContextClassLoader()来获取子加载器资源