类加载子系统就是负责从文件系统或者网络中加载class文件。类加载子系统(Class Loader)只负责Class文件的加载。
至于这个文件是否能够正常运行,它不负责管理。是由执行引擎(Execution Engine)来决定的。
而加载的类信息存放于一块称为方法区(元空间,Method Area)的内存来管理的。
类加载的过程(图解)
可以说:类加载的过程就是包括了加载、验证、准备、解析、初始化的五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则又不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
在这里也要注意到这几个阶段是按顺序开始,而不是按顺序进行或完成。 因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中会调用或激活另一个阶段。
加载.class文件的方式(了解):
● 从本地系统中直接加载。
● 通过网络获取,典型场景:Web Applet。
● 从zip压缩包中读取,成为日后jar、war格式的基础。
● 运行时计算生成,使用最多的是:动态代理技术。
● 由其他文件生成,典型场景:JSP应用。
● 从专有数据库中提取.class文件。
● 从加密文件中获取,典型的防止Class文件被反编译的保护措施。
链接(Linking)阶段分为3个步骤:验证、准备、解析。
检验被加载的类是否具有正确的内部结构,并和其他类协调一致。
也是为了确保被加载类的Class文件的二进制字节流中包含的信息符合当前虚拟机的要求。保证被加载类的正确性,不会危害虚拟机自身安全。
主要有四种验证方式:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证文件格式是否一致:
class文件在文件开头具有特定的文件标识(字节码文件都以0xCA FE BA BE标识开头的)咖啡baby这可能就是java标志是个茶杯的样子了吧,哈哈。
元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,例如这个类是否具有父类;是否继承浏览器不允许被继承的类(final修饰的类). . .
字节码校验(验证):
Java虚拟机对字节流进行数据分析,这些字节流代表的就是类的方法。
在验证中会进行大量的检查,比如:
● 保证局部变量在赋予合适的值以前不能被访问
● 类中的字段中必须总是被赋予正确类型的值
● 类的方法被调用时总是传递正确的数值和类型的参数。
符号引用验证
就是方法之间的互相调用。
如果包含在class文件中的符号引用被解析时,class文件检验器将进行第四趟检查。大多数虚拟机都采用延迟装载类的策略,只有类被真正调用的时候才会解析。所以本次验证会再第三趟扫描之后很长一段时间,字节码被执行时才进行。
● 准备阶段则负责为类的静态属性分配内存,并设置默认初始值 (比如int的默认初始值就是0。)
● 不包含final修饰的static修饰的常量,它是在编译时已经进行初始化赋值了
● 也不会为实例分配初始值。对象在这个阶段是还没有创建的,在方法区里进行具体的操作,最后都是在堆内存当中。
举例:
pubuic static int demo = 10;
在准备阶段的时候,demo的初始值为0,而不是10。在编译器才具体的赋值
0才是demo的默认初始值
将类的二进制数据中的符号引用替换成直接引用
(符号引用就是Class文件的逻辑符号,直接引用指向的方法区中某一地址)
说人话就是:将字节码中符号引用替换成直接引用
举例:
编写代码 方法1 中调用 方法2 (这就可以认为是一个符号引用)
一旦类加载到内存后把符号的引用地址换成 内存的地址引用
初始化,就是为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化赋值。(真正的给值)
对 static 修饰的变量或语句块进行赋值。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
大致顺序:
完整顺序流程就是:
父类的static --> 子类的static --> 父类的构造方法(当调用这个子类的构造方法的时候,才是会被加载到的) --> 子类的构造方法
代码的体现一下(因为是jvm底层的操作,在代码上也没有太好的体现出来)
public class Demo { static int num = 10; //准备阶段为静态的变量进行初始化,赋值(赋默认值为0)初始化阶段再赋值为10 static { num = 100; } public static void main(String[] args) { System.out.println(num); } //num的值是: 加载时为0 到达初始化。因为是static修饰的,从上到下,就先变为10,再到最后的100 }
● 站在JVM的角度来看,类加载器是可以分为两种的:
● 站在java开发者的角度来看,类加载器就应该划分的更加细致一点,自jdk1.2之后java就一直保持着三层类加载器。
这个类加载器使用的是C/C++语言来实现的,是嵌套在JVM的内部。它是用来加载 java的核心类库。
它并不继承于java.lang.ClassLoader 是没有父加载器的。
它是负责加载扩展类加载器 和 引用类加载器(系统类加载器)。并为它们指明父类的加载器。
出去安全考虑 ,引导类加载器只加载存放在<JAVA_HOME >\lib目录里面的或者被-Xbootclasspath 参数锁指定的路径中存储的类。
是由Java语言来编写的,由sun.misc.Launcher$ExtClassLoader来实现的。
它派生于ClassLoader的这个类。
所有Java方面的类加载器都是需要继承ClassLoader这个类的
它就负责加载从java.ext.dirs 系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动又扩展类加载器加载
由java语言编写的,由sun.misc.Launcher$AppClassLoader来实现的
它派生于ClassLoader的这个类。
加载我们自己定义的类,用于加载用户类路径(classpath)上的所有类。
负责加载用户类。
这个类加载器是程序中默认的类加载器。
例如Tomcat,在Tomcat之中就有着一些内置的用户级别的类加载器。
在Java中。ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(但不包括启动类加载器)
package com.wang.javaforword.jvm.classloader; public class ClassLoadDemo { public static void main(String[] args) { //获取系统的类加载器是什么 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//得到结果为程序类加载器 //获取程序类加载器的上一级是什么加载器 ClassLoader parent = systemClassLoader.getParent(); System.out.println(parent);//得到为扩展类加载器 //获取扩展类加载器的上一级是什么加载器 ClassLoader parentParent = parent.getParent(); System.out.println(parentParent); //理应为引导类加载器(启动类加载器)但是这个加载器由C写的,java无法获得,所以结果为null //自己写的类是由应用程序类加载器加载的 ClassLoader classLoader = ClassLoadDemo.class.getClassLoader(); System.out.println(classLoader); //获取到自己写的类的上一级是什么类型的类加载器 System.out.println(classLoader.getParent()); } }
Java虚拟机对Class文件采用的是按需加载的方式,也就是说当需要该类时才会将它的Class文件加载到内存中生成Class的对象。
而且加载某个类的Class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类来处理,这就是一个任务委派模式。
除引导类加载器外,所有其它类加载器都有其父类加载器。
工作原理:
如果均加载失败的话,就会抛出ClassNotFoundException(类找不到异常)
小问题:
我们自己创建一个名为 java.lang 的包,再创建一个名为 String 的类,当我们 new String()时,会将加载创建核心类库中的 String 对象还是创建我们自己创建 的 String 类对象?
在这里它因为双亲委派的机制,内加载器会向父类的方向走,通过java.lang.String最终加载的是java核心库中的String对象。
使得Java类随着类加载器不同而具备带优先级的层次关系,如java.lang.Object(位于rt.jar内),无论那个类加载器要加载该类,最终都委派给顶层引导类加载器,因此Object类在程序的各种类加载环境中都是同一个类。
如果没有双亲委派,用户自定义重名的类,将会使得系统带有多个同名的类,使得基础的Java类型体系混乱
双亲委派模型对于保证Java程序的稳定运行十分重要,它实现却很简单
首先就是检查是否被加载过,若没有加载则调用父类加载器的loadClass方法,若父类加载器为空,则默认使用引导类加载器作为父类加载器,如果加载还是失败,则调用自身的findClass()方法来加载。
破坏双亲委派情形:使用JNDI服务、代码模块热部署
JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就会有被动使用。被动使用是不会初始化的。
通过new关键字被导致类的初始化,这是大家经常使用的初始化一个类的方式,它肯定会导致类的加载并且初始化。
访问某个类或接口的静态变量,或对该静态变量赋值。(包括读取和更新)
访问类的静态方法。
反射,通过反射的方式获得类的对象,也是对类的主动使用
初始化子类会导致父类的初始化。
执行该类的main函数的时候,也就是Java虚拟机启动的时候被标记为启动类的类。
JDK1.7开始提供动态语言支持(如我们可以在JVM上使用脚本语言的引擎调用JavaScript(动态语言)代码):
java.lang.invoke.MethodHandle实例的解析结果REF_getStastic、REF_putStastic、REF_invokeStastic句柄对应的类没有初始化,则进行初始化。
以上这里都是属于类的主动使用
其实除了以上的几种主动使用,其余的都算做事被动使用。
引用该类的静态常量,注意是常量。是不会导致初始化的。但是也是存在意外的,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得到结果的常量就会导致初始化。
比如:
public final static int NUMBER = 10;
//这个就不会初始化,它属于被动使用
public final static int RANDOM = new Random().nextInt() ;
//因为通过了一定的计算,所以会导致类的初始化,为主动使用
构造某个类的数组时不会导致该类的初始化。
比如:
Student[] students = new Student[10];
主动使用和被动使用的区别就在于类调用的时候是否进行了初始化。