类加载
类加载机制
定义
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这就是虚拟机的加载机制。
类加载过程
类加载过程主要分为三个部分:
- 加载;
- 链接;
- 初始化;
其中链接可以细分为 验证、准备和解析 三个部分。
需要注意的是,这几个阶段是按顺序开始,但不一定是顺序进行或完成的,一般是相互交叉混合进行的(解析阶段在某些情况下于初始化之后开始,为了支持Java语言的运行时绑定)
引申
绑定:把一个方法的调用与方法所在的类关联起来,分为静态绑定与动态绑定:
静态绑定:程序执行前已经被绑定。java当中的方法只有final,static,private和构造方法是前期绑定的。动态绑定也叫运行时绑定,几乎所有方法都是后期绑定的。
加载
定义:将类的 .class文件 中的二进制数据 读入到内存 中,将其放在运行时数据区的方法区内,然后在堆中创建一个Class对象,用于封装类在方法区的数据结构,并向程序员提供访问方法区内的数据结构接口。
验证
目的:确保.class文件中的字节流包含的信息符合虚拟机要求,一般包括四个阶段的验证:
- 文件格式验证:保证字节流能正确解析并存储于方法区内。后面的都是基于方法区的存储结构进行。
- 元数据验证:对类中各种数据类型进行语法校验。
- 字节码验证:进行数据流与控制流分析,对类方法体进行校验分析;
- 符号引用验证:对类自身以外的信息进行校验(访问性校验)。
准备
正式为静态变量分配内存并设置默认值。(针对的是类变量,并不包括实例变量,注意默认值与初始值区别)
解析
目的:将运行时常量池中的符号引用替换为直接引用。
符号引用:即一个字符串,给出了唯一识别一个方法,一个类,一个变量的信息。引用的目标不一定在内存中;
直接引用:一个内存地址(类方法,类变量)或者是一个偏移量(实例方法,实例变量)。如果存在直接引用,则引用的目标一定存在于内存中。
初始化
开始执行类中定义的Java代码,为类的静态变量赋予正确的初始值。
因此,可以得到:
- 类初始化方法一般在类初始化时执行;
- 对象初始化方法一般在实例化类对象的时候执行。
类加载器
分类
启动类加载器
由C++实现,是虚拟机自身的一部分,用于加载<JAVA_HOME>/lib核心类库与-Xbootclasspath参数指定的jar包到内存。无父类加载器。
扩展类加载器
用于加载<JAVA_HOME>/lib/ext目录下的类库或者由java.ext.dirs系统变量指定的类库。父类加载器为null。
应用程序(系统)类加载器
用于加载 classpath 上指定的类库。系统默认类加载器。父类加载器为扩展类加载器。
自定义加载器
父类加载器为系统类加载器,一般通过继承 URLClassLoader 来实现。
注意:分类中提及的父类加载器并非继承关系,而是给定一个parent变量来指定。
双亲委派模型
工作机制:如果一个类加载器收到类的加载请求,它并不会自己去加载,而是把请求委托给父类的加载器执行,如果还存在父类,则继续递归向上委托;如果父类无法完成加载任务,子加载器才会自己尝试去加载。
优势
- 使得Java类随着其类加载器一起具有了优先级的层次关系,避免类的重复加载引起的程序混乱;
- 在与核心类重名的情况下优先选择核心类,由于只需要加载一次,在收到与核心API库重名的类时直接返回原始加载的核心API,防止核心API库被随意篡改。
JVM两个class对象是否为同一个类对象的必要条件
- 类的完整类名必须一致,包括包名;
- 加载类的ClassLoader实例对象必须相同。
自定义类加载器
自定义类加载器,只需要继承ClassLoader抽象类或者URLClassLoader类,并重写findClass方法。(如果要打破双亲委派模型,则需要重写loadClass方法)
// 自定义类加载器
public class CustomClassLoader extends ClassLoader{
private String classPath;
public CustomClassLoader(String classPath){
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
}catch (Exception e){
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 用于获取.class 的字节流
* @param name
* @return
* @throws Exception
*/
private byte[] loadByte(String name) throws Exception{
name = name.replaceAll("\\.","/");
FileInputStream fis = new FileInputStream(classPath+ "/"+ name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
public static void main(String[] args) throws Exception{
// 自定义加载的位置,使用自定义类加载器加载
CustomClassLoader loader = new CustomClassLoader("G:/tmp");
// 注意包名带来的影响
Class clazz = Class.forName("Car", true, loader);
Object o = clazz.newInstance();
Method m = clazz.getMethod("Print",null);
m.invoke(o, null);
}
}
// 注意该文件的位置在G:/tmp目录下
public class Car {
public Car() {
System.out.println("Car:" + getClass().getClassLoader());
System.out.println("Car Parent:" + getClass().getClassLoader().getParent());
}
public String print() {
System.out.println("Car:print()");
return "carPrint";
}
}
在使用自定义类加载器之前,先使用javac命令获取Car.class文件(位置也是G:/tmp目录下),再使用自定义类加载器对得到的Car.class文件进行加载。
应用
- Tomcat自定义类加载器:
自定义类加载器,以便对应用的不同版本类库进行隔离,相同版本进行共享;同时,对应用的类库与容器的类库进行隔离。
对于非.class文件,需要转化为Java类时,就需要自定义类加载器。(JSP文件) - 对Java核心代码加密:
对.class文件加密,在自定义类加载器中完成解密;