🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
#### **类的加载过程** 类从被加载到内存开始,会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、和卸载(Unloading)这7个阶段,其中验证、准备和解析3个统称为连接(Linking)。 ![](https://box.kancloud.cn/8575a19086e00b0a7e8ecb29c0bb8bfb_906x294.png) 图1 类的生命周期 其中加载、验证、准备、初始化和卸载这5个接口的顺序是确定的,但是解析阶段是不一定的,因为java语言的动态绑定。 **加载(Loading)** : 加载阶段虚拟机要完成3件事情: 1. 通过一个类的全限定名来获取定义此类的二进制流。 2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。 **类的加载时机** Java虚拟机规范中并没有强制约束什么时候开始进行加载。但是对于初始化阶段,虚拟机规范严格规定了“**有且只有5种情况必须立即对类进行初始化**”,而加载、验证、准备肯定是在初始化之前要做的,那其实也间接的约束加载,也可以理解为类的加载时机。 1. 遇到new、getstatic、pubstatic或invokestatic这4条字节码指定时,如果类没有进行初始化,需要先初始化。这4个指令对应Java代码的场景分别是:使用new关键字创建对象时、读取或者设置一个类的静态自动(不被final修饰的)、调用一个类的静态方法时。 2. 使用java.lang.reflect包中的方法对类进行反射调用时,如果没有初始化先初始化。 3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。 4. 当虚拟机启动时,用户指定一个要执行的主类,就是程序的入口,带main方法的类,那么要先初始化这个主类。 5. 当使用JDK1.7的java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这和对应的类有没初始化,要先触发初始化。 上边5种叫做主动引用,除了这5种情况其他引用类的情况不会被初始化,也叫做被动引用。 **验证(Verification)**: 验证是连接阶段的第一步,这一阶段的目的为了确保Class文件字节流中包含的信息符合虚拟机的要求。验证阶段大致会从一下4个方面验证。 1. **文件格式的验证**:验证字节流是否符合Class文件的格式规范,例如是否是以魔数0xCAFEBABE开头的、版本号是否在虚拟机的处理范围内、常量池中是否有不支持的常量类型、CONSTANT_Uff8_info类型的常量中是否有不和UTF8编码的、Class文件中各个部分及文件本身是否有删除或者附加的信息等。 2. **元数据验证**:对字节码进行分析,看是否符合Java的语言规范要求。例如是否所有的类都继承自Object、这个类是否继承了被final修饰的类、如果是抽象类是否实现了父类的方法等。 3. **字节码验证**:通过分析字节码判断代码是合法的、符合逻辑的。例如保证跳转指针不会跳转到方法体外的指令上、保证方法体重的类型转换是有效的等。一个类的方法字节码没有通过字节码验证肯定是有问题的,但是一个方法字节码通过了验证,也不能说他一定是安全的。 4. 符号引用验证:这一步是为了解析做准备,判断符号引用通过全限定名是否能找到对应的类、符号引用中的类,字段,方法的访问修饰符是否可以被当前类方法,在这一步如果如果不能通过符号引用可能会抛出如下异常: java.lang.IllegalAccessError java.lang.NoSuchFieldError java.lang.NoSuchMethodError **准备** : 准备阶段是给static修饰的变量分配内存并设置初始值的过程。这里说了是被static修饰的变量,类中的成员变量是在对象实例化的时候随着对象分配在Java堆中的。 这里设置初始值有两种情况,如果这个static的变量没有被final修饰的话那么设置的就是这个类型的默认值,直到类执行到初始化阶段才会按照代码中设定的值初始化。 如果是被final修饰的static变量会在这里阶段被设置成代码中的值,想想为什么 **解析** : 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,这里包括对类或者接口的解析、字段解析、类方法解析、接口方法解析。 **初始化** : 到了初始化才真正开始执行Java代码或者说是字节码,在准备阶段变量被设置了一次初始值,在这个阶段则需要根据代码中设定的值初始化static变量和其他的资源,也可以理解为初始化阶段是执行构造器< clinit >()方法的过程。 **< clinit >()方法**:是由编译器自动生成的,它用来执行类中所有的static变量的赋值工作和静态语句块中的代码。执行的顺序是由Java代码的出现顺序决定的,静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,它只可以进行赋值,但不能访问。 #### **Java中的类加载** ![](https://box.kancloud.cn/ceb6fdb7b50d2e4863c3593980d24d1e_729x513.png) 图2 类加载 * Bootstrap ClassLoader 负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类 * Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包 * App ClassLoader 负责记载classpath中指定的jar包及目录中class * Custom ClassLoader 属于应用程序根据自身需要**自定义的ClassLoader**,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。 #### **Dex 和动态加载类机制** **背景**: 在Android 应用开发的一般情况下,常规的开发方式和代码架构就能满足普通的需求。但是有些特殊问题,常常引发进一步的思考。例如应该怎么样开发一个可以自定义控件的Android应用?就像Eclipse 一样,可以**动态加载插件**。如何**让Android 应用执行服务器上的不可预知的代码**?如何**对Android 应用加密,而只在执行时白解密,从而防止被破解**?上述问题,我们**可以使用类加载器来灵活的加载执行的类**。 **类加载机制** Dalvik 虚拟机如同其他Java 虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在标准的Java 虚拟机中,类加载可以从class 文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在**程序运行时手动力日载Class ,从而达到代码动态加载执行的目的。** 然而Dalvik 虚拟机毕竟不算是标准的Java 虚拟机,因此在类加载机制上,它们有相同的地方,也有不同的地方。我们必须区别对待。例如当在使用标准Java 虚拟机时,我们经常自定义继承自ClassLoader 的类加载器。然后通过defineClass 方法来从一个二进制流中加载Class 。然而,这在Android 里是行不通的,大家就没必要走弯路了。参看源码我们知道, Android 中ClassLoader 的defineClass 方法具体是调用VMC lassLoader 的defineClass 本地静态方法。而这个本地方法除了抛出一个“ UnsupportedOperationException ”之外,什么都没做,甚至连返回值都为空。 **Dalvik 虚拟机类加载机制** 那如果在Dalvik 虚拟机里, ClassLoader 不好使,我们如何实现动态加载类呢?Android 为我们从ClassLoader 派生出了两个类: DexClassLoader 和PathClassLoader。