zoukankan      html  css  js  c++  java
  • 深入理解Java虚拟机(八)——类加载机制

    是什么是类加载机制

    Java虚拟机将class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是类加载机制。

    类的生命周期

    一个类从加载到内存开始,到卸载出内存为止,一共经历七个阶段:
    加载——>验证——>准备——>解析——>初始化——>使用——>卸载

    其中,类加载包括其中五个阶段:
    加载——>验证——>准备——>解析——>初始化

    其中三个过程被称为连接:
    验证——>准备——>解析

    C/C++需要在运行前就完成代码文件的预处理、编译、汇编和连接,而Java可以在运行期间完成类加载任务。虽然这样会提高运行期间的计算开销,但是可以提高程序的灵活性。Java语言的灵活性体现在它可以在运行期间动态扩展,所谓动态扩展就是在运行期间动态加载和动态连接。

    类加载的时机

    类加载的顺序

    类加载过程中的加载、验证、准备、初始化是顺序按部就班地开始的,但不一定是顺序地结束,因为它们会相互交叉混合地进行,会在一个阶段执行地过程中调用激活下一个阶段。

    解析有可能会发生在初始化阶段时候开始。

    类加载中“初始化”开始的时机

    《Java虚拟机规范》没有规定“加载”的触发条件,而严格规定了只有六种情况下必须进行类的“初始化”:

    1. 如果在运行过程中遇到以下字节码指令,类型没有被初始化,那就需要触发其初始化阶段:new、getstatic、putstatic或invokestatic。以下情况下会生成这四条指令:
      • 使用new关键字实例化对象;
      • 读取或设置一个类型的静态字段(不包括final修饰的常量);
      • 调用一个类型的静态方法的时候。
    2. 使用java.lang.reflect包的方法对类型进行反射调用的时候。
    3. 初始化类的时候,其父类没有初始化,就要先初始化其父类。
    4. 当虚拟机启动的时候,虚拟机先去初始化main()方法的主类;
    5. 加入的动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
    6. 如果一个接口中定义了默认方法(被default修饰),如果这个接口的实现类发生了初始化,那么这个接口需要被先初始化。

    主动引用和被动引用

    Java程序触发了上述六种条件之一,就被称为对一个类型进行主动引用

    其他的所有引用类型的方式都不会触发初始化,被称为被动引用

    被动引用的示例

    1. 通过子类调用父类的静态字段
    public class Fu{
        public static String value= "233";
        static{
            System.out.println("父类被初始化!");
        }
    }
    
    public class Zi{
        static{
            System.out.println("子类被初始化!");
        }
    }
    
    public static void main(String[] args){
        System.out.println(Zi.value);
    }
    

    当调用静态字段的时候,只有静态字段的所属类会被初始化。通过子类调用父类的静态字段,就是被动引用子类。

    1. 通过数组定义来引用类
    public class Main{
    	public static void main(String[] args) {
    		SuperClass[] sc= new SuperClass[10];
    	}
    }
    

    这里其实只初始化了一个数组,而没有初始化数组中的元素,每个类还是空的,不能被使用。

    1. 调用类中的常量(final)

    接口的加载

    接口的加载过程与类的有所不同,不同点在于:类初始化的时,如果发现父类没有被初始化,会先去初始化父类;而接口并不要求其父类全都完成了初始化,只有在真正用到的时候才会去初始化。

    类加载的过程

    类加载的全过程分别时加载、验证、准备、解析和初始化五个阶段。

    1.加载

    “加载”是整个类加载的第一个阶段,是堆文件的数据加载,二者不能混淆。

    加载的任务

    在加载阶段,Java虚拟机主要完成三件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流。
    2. 将这个字节流所代表的存储结构转化为方法区的运行时数据结构。
    3. 在内存中生存一个java.lang.Class对象,用这个对象作为方法区中这个类的外部访问入口。

    获取加载的源数据途径

    虚拟机规范只是说需要通过一个类的全限定名称来获取该类的二进制流,而没有说要从哪里获取如何获取,所以,就有很多选择:

    • 从压缩包中读取,jar、ear、war格式的包。
    • 从网络中获取,web applet。
    • 运行时计算生成,动态代理技术,通过反射。
    • 其他文件生成,从JSP文件中生成Class文件。
    • 从数据库读取,把程序安装到数据库中以完成代码在集群间的分发。
    • 从加密文件中获取,为了防止Class文件被反编译。

    数组类型加载的特殊性

    数组类本身不通过类加载器创建,而是由Java虚拟机直接在内存中动态构造出来的。

    数组类和类加载器具有密切的关系,因为数组的元素还是需要通过类加载器完成加载,数组类创建过程遵循以下规则:

    • 如果数组的组件类型(指的是数组去掉一个维度的类型,不一定就是数组的元素类型)是引用类型,则去递归采用类加载器去加载这个组件类型。
    • 如果组件类型不是引用类型,例如原子类,则虚拟机则会将数组标记与引导类加载器关联。
    • 数组的可访问性与其组件类型的可访问性一致,如果组件类不是引用,则其默认访问性是public。

    加载过程的特点

    1. 加载过程中将二进制字节流放入方法区中,其格式是完全由虚拟机自行制定的。
    2. 加载阶段和连接阶段的部分动作是交叉进行的。开始时间还是保持现后顺序。
    3. 类数据放置到方法区之后,堆内存中就会实例化一个java.lang.Class类的对象,通过这个对象作为程序访问方法区中类型数据的外部接口。

    2.验证

    验证时连接阶段的第一步,不一定需要验证,如果类被反复验证和使用过了,可以关闭验证以提高运行效率。

    验证过程的任务

    确保Class文件中的字节流中包含的信息符合Java虚拟机规范的全部约束要求,以保证这些信息不会为虚拟机造成危害。

    为什么需要验证

    在操作系统中,防止缓冲区溢出攻击有两种预防策略,一种是编译时防御系统,另一种是运行时防御系统。

    类加载中的对代码的验证就是编译时防御系统。这样可以防止一些不安全的行为,恶意代码的攻击,例如访问数组边界以外的数据,跳转到不存在的代码行。如果出现这些问题,编译就无法通过,抛出异常。

    验证的过程

    1. 文件格式验证
      验证字节流是否符合Class文件的格式规范,并且符合当前版本的处理范围。这一阶段的验证包括是否以魔数开头、版本信息是否满足虚拟机要求、常量池中是否存在不被支持的常量类型等。
      只有通过了这个阶段的验证之后,字节流才能被放入方法区存储。而上个加载阶段也是将字节流从class文件中存到方法区,这就可以看出,验证和加载时交叉进行的。
    2. 元数据验证
      对字节码描述的信息进行语义分析。例如这个类是否有父类、是否继承了不允许被继承的类等等。
    3. 字节码验证
      最复杂的阶段。主要的目的时通过数据流的分析和控制流的分析,确定程序语义是否是合法、符合逻辑的。保证方法在运行的时候不会出现安全问题。例如跳转指令不会跳到方法意外的字节码指令上。
    4. 符号引用验证
      本阶段验证发生在连接的解析阶段,在虚拟机将符号引用转化为直接引用的时候。确保引用的可用性。例如字符串全限定名是否能找到对应的类。

    3.准备

    准备阶段的任务

    正式为类中定义的变量分配内存并设置初始值。

    准备阶段的注意点

    1. 在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
    2. 这是时候内存分配的仅包括类变量,而不包括实例变量,实例变量会随着对象的创建分配到堆上。这里的初始值通常是零或者null。如果是常量,就会被赋上实际的值。
    public static String name = "lippon";
    

    在这个准备阶段,静态变量name的值被在方法区中分配内存,同时附上null的初值。

    public static final String name = "lippon";
    

    而常量在准备阶段就会被赋上真正的值。

    4.解析

    解析阶段的任务

    是Java虚拟机将常量池中的符号引用替换为直接引用的过程。

    解析阶段的要点

    符号引用和直接引用

    • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能够定位到目标即可。
    • 直接引用:是可以直接指向目标的指针、相对偏移量或者是一个间接定位到目标的句柄。

    符号引用时固定的,直接引用是根据内存变化的。

    5.初始化

    是类加载过程中的最后一个步骤 ,其他的步骤完全由Java虚拟机来主导。直到初始化阶段,虚拟机才开始真正运行编写的Java程序代码,并将主导权给应用程序。

    初始化的任务

    初始化就是执行类构造器clinit()的过程。
    clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

    初始化过程的要点

    1. clinit()方法中的顺序是语句在源文件出现的顺序决定的。
    2. 静态语句块只能访问到定义在静态语句块之前的变量,之后的不能访问,有点像C语言。
    3. clinit()方法与类的构造函数init()方法不同,它不需要显式地调用父类地构造器,而是由虚拟机保证父类的clinit()被执行完毕。因此在Java虚拟机中第一个被执行的clinit()方法的类型肯定是java.lang.Object 。
    4. 由于父类的clinit()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
    5. clinit()方法不是必须的,如果类中没有静态语句块,也没有对变量的赋值操作,就可以不生成该方法。
    6. 接口中不能使用静态语句块,但仍然有变量的初始化赋值,所以,接口也会有clinit()方法被生成。
    7. 接口与类不同的是,接口的clinit()方法执行之前不需要执行其父接口的clinit()方法,当父接口的变量被使用的时候,父接口才会被初始化。
    8. 接口的实现类在初始化时也不会执行接口的clinit()方法。
    9. 虚拟机会给clinit()方法加锁,防止多线程同时执行这个方法。如果这个方法被一个线程执行了,其他等待的线程就不会再区执行这个方法。

    类加载器

    类与类加载器

    类加载器的作用:将class文件加载进入JVM的方法区,并在方法区中创建java.lang.Class对象作为外界访问这个类的接口。还用于确立这个类在Java虚拟机中的唯一性。

    • 比较两个类是否相同,只有将这两个类通过同一个类加载器加载才有意义。如果两个类文件被不同的类加载器加载,这个两个类必定不同。

    类加载器种类

    • 启动类加载器 :负责加载Java_Homelib中的class文件。
    • 扩展类加载器:负责加载Java_Homelibex中的class文件。
    • 应用程序类加载器:负责加载用户classpath下的class文件。
      在这里插入图片描述

    双亲委派模型

    工作过程

    如果一个类加载器接收到类加载的请求,首先会让自己的父类加载去为加载这个类,如果父类加载是被,就自己去完成加载。

    作用

    Java中的类随着它的类加载器一起具备了一种优先级的层次关系。无论使用哪个类加载器,最终都会委派给最顶端的加载器加载,从而使得不同的类加载器加载的Object类都是同一个。

    原理

    双亲委派机制的源码,全部集中在java.lang.ClassLoader的loadClass()方法之中,其逻辑入下:

    1. 首先检查请求的类是否已经被加载过了;
    2. 如果该类加载器存在父,则将请求交给父加载器;
    3. 如果父加载器无法加载,则由本身加载;
    4. 最后返回加载的对象。
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
    	// 首先,检查请求的类是否已经被加载过了
    	Class c = findLoadedClass(name);
    	if (c == null) {
    		try {
    		if (parent != null) {
    			c = parent.loadClass(name, false);
    		} else {
    			c = findBootstrapClassOrNull(name);
    		}
    	} catch (ClassNotFoundException e) {
    		// 如果父类加载器抛出ClassNotFoundException
    		// 说明父类加载器无法完成加载请求
    	}
    	if (c == null) {
    			// 在父类加载器无法加载时
    			// 再调用本身的findClass方法来进行类加载
    			c = findClass(name);
    		}
    	}
    	if (resolve) {
    		resolveClass(c);
    	}
    	return c;
    }
    

    缺点

    1. 当基础类需要回调用户代码。
    2. 无法满足动态性。
    3. 如果需要加载类名相同的项目文件,让让父类去加载就只能加载一份。

    双亲委派的破坏

    为什么要破坏双亲委派?
    因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件。

    破坏双亲委派的例子

    • tomcat
      自己定义了一个类加载器,可以加载对应war里的class文件。 当然了, 其他的项目文件, 还是要委托上级加载的。
    • jdbc

    模块化系统

    JDK 9中虽然仍然维持着三层类加载器和双亲委派的架构,但类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载,也许这可以算是对双亲委派的第四次破坏。
    在这里插入图片描述

  • 相关阅读:
    iOS 9音频应用播放音频之第一个ios9音频实例2
    隐写术: 基地组织如何用视频隐藏绝密文档?
    stm32 Fatfs 读写SD卡
    STM32应用笔记转载
    STM32-NVIC中断管理实现[直接操作寄存器]
    FatFs源码剖析
    STM32固件库详解(转)
    Arduino线程库ProtoThreads
    ARM9(TQ2440)裸机代码分享
    将USBASP改造成STK502编程器(转)
  • 原文地址:https://www.cnblogs.com/lippon/p/14117683.html
Copyright © 2011-2022 走看看