理解类加载机制
类加载运行的过程
类加载运行大概过程图如下:
类加载的过程(loadClass)
一个类被编译成class文件后,执行主方法的类加载过程会进行如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载
- 在硬盘上查找文件,并通过IO流的方式读入字节码文件。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
tips:在加载阶段获取二进制字节流时,可通过自定义类加载器来进行加载。
验证
- 文件格式验证:Class文件是否符合规范,如:是否以模数0xCAFEBABE开头,主次版本号是否在当前运行环境的处理范围内等。
- 元数据验证:对字节码描述的信息进行分析,保证其符合Java规范要求,如:这个类是否有父类,继承的父类是否被final修饰不允许继承,是否实现了接口的所有方法等。
- 字节码验证:通过数据流和控制流分析,确定程序的语义是否是合法,符合逻辑,如:保证方法中的类型转换有效,保证操作数栈的数据类型与指令代码序列都能配合工作(如:操作数栈放入int,而使用时按long来加载本地变量表)。
- 符号引用验证:类自身以外的信息进行匹配校验,如:符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;符号引用中的类、字段、方法的访问性(private,protected,public,default)是否可以被当前类访问。
准备
给类的静态变量分配内存,并赋予默认值。
- 只对static修饰的静态变量进行内存分配,赋予默认值,这里的默认值不是定义的初始值,而是 0,null,false等等。
- 对final的静态字面值常量直接赋予初始值,如果不是静态字面值常量,直接赋予默认值(静态字面值常量:0, 0.1, ‘a’,”hello“等不会进行改变的常量(写死的值))。
解析
将常量池(方法区)中的符号引用替换为直接引用的过程。
- 符号引用就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
- 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化
这时才会对静态变量赋予初始值,其中有两种方式:
- 定义变量时直接指定,如 private static int a = 10。
- 在静态代码块里为静态变量赋值,如 static { a = 10 }。
卸载
- 执行 System.exit()方法。
- 程序执行结束。
- 执行过程中遇到异常或错误导致Java虚拟机停止。
- 由于操作系统出现错误导致Java虚拟机进程停止。
类加载信息
类被加载到方法区中主要包含:运行时常量池,类型信息加粗样式,字段信息,方法信息,类加载器的引用,对应class实例的引用等。
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
tips:jar包或war包的类并不是一次性全部加载,而是在主类运行过程中使用到时,会逐步进行加载。
类加载器和双亲委派机制
类加载器分类
类加载过程由类加载器来实现,常见的类加载器有:
- 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等。
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
- 自定义加载器:负责加载用户自定义路径下的类包。
类加载器初始化过程
在创建JVM启动器会实例化sun.misc.Launcher(由C++内部生成引导类加载器来加载),sun.misc.Launcher初始化使用单例模式设计,其目的是保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。
在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。 JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。
双亲委派机制
JVM类加载器有自己的层级结构,见下图:
双亲委派机制:当加载某个类时,会先委托父加载器寻找目标类,找不到时再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并加载目标类。
比如程序中有个User类,由应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到User类,则向下退回加载User类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到User类,又向下退回User类的加载请求给应用程序类加载器, 应用程序类加载器于是在自己的类加载路径里找User类,然后进行加载。
tips:如果这个类已经被加载过,如已经被应用程序类加载器加载,那么下一次再次加载时,就会直接用应用程序类加载器加载。(具体源码参见ClassLoader的loadClass方法)
全盘负责委托机制:当一个ClassLoader装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。
双亲委派机制设计的原因
- 沙箱安全机制:防止核心的Api库被篡改覆盖,假如我创建一个java.lang.Math类,这个类只会被父加载器加载,回传不到应用程序加载器或自定义加载器。
- 避免类的重复加载:如果父加载器已经加载了某个类,就没有必要子加载器再次加载,保证被加载的类的唯一性。
自定义加载器
自定义加载器需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
步骤为:
- 将指定路径下的.class文件作为 new FileInputStream的参数,得到class文件的输入流IO。
- 将IO读取到字节数组中,通过defineClass方法(将字节数组传为Class对象),返回Class对象。
打破双亲委派机制
利用自己的自定义加载器重写loadClass的逻辑,使其不委派给父加载器加载,由自己的自定义加载器直接findClass。