前几天本人正在愉快的写代码的时候忽然接到老大给的一个新任务,对支付相关的几个类作代码加密和安全性校验工做,确保类来源的安全性。 java
那么如今有了需求下一步就要来知足需求,这里采用的方案是在加载过程当中进行类来源检测和代码解密的相关工做。接下来主要就是实现了一个类加载器。 程序员
通过一通操做终于实现好了这个加载器,通过测试也知足了类的相关解密和校验工做,可谓是完美。然而,帅不过三秒,接下来运行的时候傻眼了,报了无数以前没有的错,发生在这个对象的equals()方法、isAssignableFrom()方法、isInstance()方法上。 数据库
如:经过自定义加载器加载的对象使用instanceof关键字作对象所属关系断定时都为false。 编程
最终经过查阅学习,从JVM的类加载机制上找到了解释。安全
类加载器能够说是Java语言的一项创新,也是构成Java平台无关性的一块基石。网络
首先明确一下类加载器是什么,根据虚拟机设计团队的解释,“实现经过一个类的全限定名来获取描述此类的二进制字节流这个动做的代码模块”被称为类加载器。数据结构
类加载器虽然只用于实现类的加在动做,可是它在Java程序中起到的做用却远远不限于类加载阶段。编程语言
对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性。这就解释了为何上面判断时为出现false,由于一个使用的系统提供的类加载器,而另外一个是使用了本身编写的加载器。ide
下面经过一个简单的实例来还原下前面的问题:学习
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文件、字段表、方法表均可以有属性表集合,用于表述某些场景下专有的信息 |
其中u一、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中的双亲委派模型。