前面提到,java代码编译成字节码后,由jvm加载并执行,那么jvm加载的过程是由类加载子系统来执行的。java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最后形成可以被虚拟机直接使用的java类型,这个过程被称为虚拟机的类加载机制。
类从被加载到内存到卸出内存,分为以下几个过程
加载连接 验证准备解析 初始化使用卸载这里的加载指的是加载过程中把字节码从文件中的结构加载到内存中的数据结构的一步,而不是完整的加载过程。这个是运行的第一步。jvm的加载时机并没有明确约束,所以一般都是各个虚拟机来实现。目前jvm可以从各个途径来获取字节码文件。加载阶段,jvm会做以下处理: 1.通过一个类的全限定名来获取该类的二进制字节流。 2.将这个字节流的所代表的静态结构转化称方法区的运行时数据结构。 3.在内存中生存一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种数据的访问入口。
由于这里没有明显的约束,所以灵活性较大,比如从哪里获取二进制字节流就有很多途径。
从zip压缩包获取,jar,war,ear等都是这一类网络传输获取,早期的Web Applet运行过程中生产,java的动态代理技术数据库获取加密文件获取,通过自定义类加载器来解密文件生成字节码,保护程序。把字节码加载到内存后,就进入到了连接阶段,而连接过程首先是对字节流进行验证,确保符合java虚拟机规范,来保证字节码格式正确或者排除部分恶意攻击意图。 验证过程主要有以下四个动作。
文件格式验证 字节码文件都是以0xCAFEBABE开头,主次版本后需要在当前虚拟机接受范围…元数据验证 语义分析,是否有父类,类是否允许被继承,抽象类实现类是否实现相关方法…字节码验证 比较复杂的一个阶段,会类的方法体逻辑进行验证,例如类型转换是否有效,字节码指令跳转异常等。符号引用验证 验证符号引用等否找到对应类,类权限是否能被引用等。准备过程是为类变量(静态变量)分配内存和初始化值的阶段.java8后这些类变量和Class对象都是放在堆中了。这里初始化变量值是数据类型的零值,不是实际值,此外这里还没有创建实例变量,所以仅仅是对类变量进行内存的分配。例如:
private static int value = 10;value值在这个阶段的数值化是0而不是10。但是如果时final修饰的静态变量,则在准备阶段直接赋值为10.
解析阶段时将java虚拟机中变量池中的符号引用替换为直接引用的过程。简单来说就是把类名限定转换为实际内存中的Class对象引用。
初始化阶段就是执行类构造器()方法的过程,这里的是java编译器生成在字节码中的,它会对类变量进行实际赋值和执行静态语句块。如果代码中没有静态变量和静态语句块,那个就没有这个类构造器。
java类加载过程的加载阶段是通过类加载器来加载的,这里为程序提供了很大的灵活且。类加载器从层级来分分为以下:
引导类加载器 c++编写的,加载<JAVA_HOME>\lib中的目录,如rt.jar,tools.jar扩展类加载器 加载<JAVA_HOME>\ext目录下的jar包文件,提供扩展能力应用类加载器 系统默认加载写自己写的代码编译后的的字节码文件。自定义类加载器 由我们自定义的加载器,加载字节码,比如加载加壳后的文件。类加载器的层级关系如下,他们是父子层级关系,但是并不是继承来实现的,因为启动类加载器是由c++来写的,而其他类加载器都是只是继承于java.lang.ClassLoader的,这里是通过组合来实现的逻辑关系。
而类加载是由哪一个类加载器加载的机制也成为双亲委派机制: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派到父类加载器来完成,每一个层次都是如此,因此最后的加载请求都是传送到最顶层的启动类加载器,只有当父类加载无法加载这个类时(这个类不是自己的加载范围),子加载器才会尝试自己去完成加载。 双亲委派机制非常重要,例如我们的代码中尝试自己写另一个包路径下的java.lang.String类,最后加载的都是java类库中的String类,因为双亲委派机制的原理,实际这个类时由启动类加载器去加载javaHome的String类。这样保证了核心类库不会被篡改从而影响系统性能和安全。