前几天本人正在愉快的写代码的时候突然接到老大给的一个新任务,对支付相关的几个类做代码加密和安全性校验工作,确保类来源的安全性。
那么现在有了需求下一步就要来满足需求,这里采用的方案是在加载过程中进行类来源检测和代码解密的相关工作。接下来主要就是实现了一个类加载器。
经过一通操作终于实现好了这个加载器,经过测试也满足了类的相关解密和校验工作,可谓是完美。然而,帅不过三秒,接下来运行的时候傻眼了,报了无数之前没有的错,发生在这个对象的equals()方法、isAssignableFrom()方法、isInstance()方法上。
如:通过自定义加载器加载的对象使用instanceof关键字做对象所属关系判定时都为false。
最终通过查阅学习,从JVM的类加载机制上找到了解释。
类加载器可以说是Java语言的一项创新,也是构成Java平台无关性的一块基石。
首先明确一下类加载器是什么,根据虚拟机设计团队的解释,“实现通过一个类的全限定名来获取描述此类的二进制字节流这个动作的代码模块”被称为类加载器。
类加载器虽然只用于实现类的加在动作,但是它在Java程序中起到的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。这就解释了为什么上面判断时为出现false,因为一个使用的系统提供的类加载器,而另一个是使用了自己编写的加载器。
下面通过一个简单的实例来还原下前面的问题:
package com.sherry; import java.io.InputStream; public class Loader { public static void main(String[] args) throws Exception { ClassLoader loader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String className = name .substring(name.lastIndexOf('.') + 1) + ".class"; InputStream iStream = getClass().getResourceAsStream( className); if (iStream == null) { return super.loadClass(name); } byte[] b = new byte[iStream.available()]; iStream.read(b); return defineClass(name, b, 0, b.length); } catch (Exception e) { throw new ClassNotFoundException(name); } } }; Object obj = new Loader(); System.out.println("默认:" + obj.getClass()); Object myObj = loader.loadClass(Loader.class.getName()).newInstance(); System.out.println("自定义:" + myObj.getClass()); System.out.println("--------------------自定义------------------------"); System.out.print("instanceof: "); System.out.println(myObj instanceof com.sherry.Loader); System.out.println("--------------------默认-------------------------"); Object obj1 = new Loader(); System.out.print("instanceof: "); System.out.println(obj1 instanceof com.sherry.Loader); } }
运行结果:
这段代构造了一个可以加载自己所在路径下的class文件的类加载器,然后通过它实例化了Loader类的一个对象myObj,同时也通过默认的方法构造获得一个对象obj,从结果可以看出,myObj这个对象确实是类com.sherry.Loader实例化的对象,但这个对象与类com.sherry.Loader做所属类型检查时却返回了false。
原因就是虚拟机中存在了两个Loader类,一个由系统应用程序类加载器加载,另一个由自定义加载器加载,虽然来源于同一个class文件,但确是两个独立的类。
到这里基本就弄明白了我们前面所遇到的问题,但是孔子曰:“学而不思则罔,思而不学则殆”。为了今后在Java类加载这里不再有更多的问题,我们还要进一步来了解更多关于类加载的知识。
借用书上的一句话:“代码编译的结果从本地机器码转变为字节码,是存储格式的一小步,确实编程语言的一大步”。
要讲解类加载机制,就不得不先了解一下类的文件结构。
想必“平台无关性”的概念大家都很熟悉,而实平台无关的这个理想最终也实现在了操作系统的应用层上。
Sun公司以及其它虚拟机提供商发布了很多可以运行在各种不同平台是的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的程序存储格式—— 字节码 。
字节码就是构成这种无关性的基石。
Java虚拟机 不和任何包括Java在内 的语言绑定,它之与“Class文件”这种特定的二进制文件格式所关联,任何一种功能性语言都可以表示为一个能够被Java虚拟机所接受的有效地Class文件。
Class文件中包含了Java虚拟机指令集和符号表以及若干其它辅助信息。
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧密的排列在Class文件中,中间没有任何分隔符,即Class文件是一个有强制性语法和结构化约束的。
Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数:属于基本数据类型,可以用来描述数字、索引引用、数量值或utf-8编码的字符串值;
表:由多个无符号数或者其他表构成的复合类型。
Class 文件格式
序号 | 类型 | 名称 | 数量 | 功能 |
---|---|---|---|---|
1 | u4 | magic | 1 | 魔数,验证 Class 文件的合法性 |
2 | u2 | minor_version | 1 | 次版本号 |
3 | u2 | major_version | 1 | 主版本号 |
4 | u2 | constant_pool_count | 1 | 常量池容量.唯一从1开始计数的;第0项常量通常为了表示“不引用任何一个常量池项目”的含义 |
5 | cp_info | constant_pool | constant_pool_count - 1 | 常量池信息 |
6 | u2 | access_flags | 1 | 访问标志,用于表示一些类或接口的访问信息。比如:是类还是接口、访问权限、是否为抽象、类是否为final |
7 | u2 | this_class | 1 | 类索引,确定这个类的全限定名 |
8 | u2 | super_class | 1 | 父类索引,确定父类的全限定名 |
9 | u2 | interfaces_count | 1 | 接口计数器 |
10 | u2 | interfaces | interfaces_count | 接口索引信息 |
11 | u2 | fields_count | 1 | 字段表计数器 |
12 | field_info | fields | fields_count | 字段表,用于描述接口或类中声明的变量 |
13 | u2 | methods_count | 1 | 方法表计数器 |
14 | method_info | methods | methods_count | 方法表,用于描述类中方法信息以及编译器自动添加的方法信息,父类中的方法如果没有被复写,则不会出现 |
15 | u2 | attributes_count | 1 | 属性表计数器 |
16 | attribute | attributes | attributes_count | 属性表,在Class文件、字段表、方法表都可以有属性表集合,用于表述某些场景下专有的信息 |
其中u1、u2等表示1个字节、2个字节的无符号数;_info结尾的表示一个表。
功能栏简要介绍了每个字段的意义,关于这些字段的具体含义,会在接下来的文章中仔细介绍。
Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一,这是进一步理解虚拟机执行引擎的基础知识。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校、转换解析和初始化,最终形成了可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括: 加载、验证、准备、解析、初始化、使用、卸载 7个阶段,其中验证、准备、解析部分统称为连接。
其中加载、验证、准备、初始化和卸载这5个阶段的 开始顺序 是确定的,而解析阶段则不一定,某些时候可以显出石化后解析(为了支持Java语言的动态绑定)。
在加载阶段,虚拟机需要完成以下三件事情:
通过一个类的全限定名来获取定义这个类的二进制字节流;
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口。
其中“通过一个类的全限定名来获取定义这个类的二进制字节流”不局限于从一个Class文件中获取,还可以从ZIP包中读取、从网络中获取、运行时计算获取、数据库获取等出多途径。
这一阶段的目的是确保Class文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致上会完成下面4个检验动作:
文件格式验证——是否符合Class文件格式的规范;
元数据验证——对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求;
字节码验证——目的是通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的;
符号引用验证——校验发生在虚拟机将符号引用转化为直接引用的时候,是对类自身以外的信息进行匹配性校验。
准备阶段是正式为类变量分配内存并设置 类变量 初始值的阶段 ,这些变量所使用的内存都将在方法区中进行分配。
注意这里分配的只是类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
这里所谓的赋的初始值一般是指数据类型的零值。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程;
符号引用:符号引用以一组符号来描述所引用的目标;
直接引用:至及诶引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到目标的句柄。
解析动作主要是针对类或接口、字段、接口方法、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
到了初始化阶段,才真正开始执行类中定义的Java程序代码;
在准备阶段中,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序指定的主观计划去初始化变量和其它资源。
上面简要的介绍了Class文件的结构、如何将类加载到虚拟机中这些问题,接下来一篇会对其中的细节再做深入介绍并介绍一下Java中的双亲委派模型。