类的加载机制过程
- 加载(loading)
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 连接(linking)
- 验证(verification): 确保该类的字节码文件中所包含的信息是否符合当前虚拟机的要求,不包含有危害虚拟机的信息(主要有四种验证,文件格式验证,元数据验证、字节码验证,符号引用验证)
- 准备(preparation): 为类变量分配内存,并设置一个初始值。除了被final修饰的类变量(即: 被 static final 修饰),该类型会在编译期就已经被分配并确定
- 解析(resolution): 将常量池中符号间接引用替换成直接引用。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替
- 初始化(initialization): 为类变量、静态代码块进行真正初始化(赋值操作)
类的初始化顺序,如果有父类先初始化父类中类变量和静态代码块,再初始化子类的静态变量、静态代码块
- 使用(using)
- 卸载(unloading)
类的初始化顺序
- 静态: 父类优先, 静态变量和代码块按照顺序加载
- 非静态: 父类优先, 成员变量和代码块按照顺序加载
- 构造函数: 父类优先
类的引用
主动引用(会执行初始化)
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用java.lang.reflect包的方法对类进行反射调用
- 当虚拟机启动,java Hello,则一定会初始化Hello类。说白了就是先启动main方法所在的类
- 当初始化一个类,如果其父类没有被初始化,则先会初始化他的父类
被动引用(不会执行类初始化)
- 当访问一个静态域时,只有真正声明这个域的类才会被初始化。例如:通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在编译阶段就存入调用类的常量池中了)
- 通过类名获取 Class 对象,不会触发类的初始化
- 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化
- 通过 ClassLoader 系统类加载器加载指定类时,也不会触发初始化动作
public class AnswerApp {
public static void main(String[] args) {
// 当访问一个静态域时,只有真正声明这个域的类才会被初始化, 因此子类 YellowPeople 并不会被初始化
// System.out.println(YellowPeople.age);
// 通过数组定义类引用,不会触发此类的初始化
// People[] peoples = new People[10];
// 引用常量不会触发此类的初始化
// System.out.println(People.num);
// 通过类名获取 Class 对象,不会触发类的初始化
// Class<People> peopleClass = People.class;
// Class<?> peopleClass = Class.forName(People.class.getName(), false, ClassLoader.getSystemClassLoader());
Class<?> peopleClass = ClassLoader.getSystemClassLoader().loadClass(People.class.getName());
}
}
class People{
static int age = 3;
static final int num = 20;
static {
System.out.println("People被初始化了!");
}
public People() {
System.out.println("父类 构造函数");
}
}
class YellowPeople extends People {
static {
System.out.println("YellowPeople 被初始化了!");
}
public YellowPeople() {
super();
System.out.println("子类 构造函数");
}
}
类加载器的原理
类缓存
标准的Java SE类加载器可以按要求查找类,一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,JVM垃圾收集器可以回收这些Class对象。
类加载器的分类
- 引导类加载器(bootstrap class loader)
- 它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader
- 加载扩展类和应用程序类加载器。并指定他们的父类加载器
- 扩展类加载器(extensions class loader)
- 用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar 或 java.ext.dirs 路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类
- 由sun.misc.Launcher$ExtClassLoader实现
- 应用程序类加载器(application class loader)
- 它根据 Java 应用的类路径(classpath 或 java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的
- 由sun.misc.Launcher$AppClassLoader实现
- 自定义类加载器
- 开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求
java.class.ClassLoader类
作用
- java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例
- ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等
常用方法
- getParent() 返回该类加载器的父类加载器。
- loadClass(String name) 加载名称为 name的类,返回的结果是java.lang.Class类的实例。此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到;从parent ClassLoader[ExtClassLoader]中加载;如果没有加载到,则从Bootstrap ClassLoader中尝试加载(findBootstrapClassOrNull方法), 如果还是加载失败,则自己加载。如果还不能加载,则抛出异常ClassNotFoundException。
- findClass(String name) 查找名称为 name的类,返回的结果是java.lang.Class类的实例。
- findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
- defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果java.lang.Class类的实例。这个方法被声明为 final的。
- resolveClass(Class<?> c) 链接指定的 Java 类。
类加载器的代理模式
代理模式即是将指定类的加载交给其他的类加载器。常用双亲委托机制。
双亲委托机制
某个特定的类加载器接收到类加载的请求时,会将加载任务委托给自己的父类,直到最高级父类引导类加载器(bootstrap class loader),如果父类能够加载就加载,不能加载则返回到子类进行加载。如果都不能加载则报错ClassNotFoundException
双亲委托机制是为了保证 Java 核心库的类型安全。这种机制保证不会出现用户自己能定义java.lang.Object类等的情况。例如,用户定义了java.lang.String,那么加载这个类时最高级父类会首先加载,发现核心类中也有这个类,那么就加载了核心类库,而自定义的永远都不会加载。
值得注意是,双亲委托机制是代理模式的一种,但并不是所有的类加载器都采用双亲委托机制。在tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。
自定义类加载器
自定义类加载器的流程
- 首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2。
- 委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3。
- 调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转抛异常,终止加载过程(注意:这里的异常种类不止一种)。
注意:
被两个类加载器加载的同一个类,JVM认为是不相同的类
public class FileSystemClassLoader extends ClassLoader {
// 根目录
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* @MethodName findClass
* @Descrition 加载类
* @Param [name]
* @return java.lang.Class<?>
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);//查询该类是否已经被加载过
if(loadedClass != null){ //该类已经被加载过了,直接返回
return loadedClass;
}else{ //该类还没有被加载过
ClassLoader classLoader = this.getParent();//委派给父类加载
try {
loadedClass = classLoader.loadClass(name);
} catch (ClassNotFoundException e) {
// e.printStackTrace();
}
if(loadedClass != null){ //父类加载成功,返回
return loadedClass;
}else{
byte[] classData = getClassData(name);
if(classData == null){
throw new ClassNotFoundException();
}else{
loadedClass = defineClass(getName(),classData,0,classData.length);
}
}
}
return loadedClass;
}
/**
* @MethodName getClassData
* @Descrition 根据类名获得对应的字节数组
* @Param [name]
* @return byte[]
*/
private byte[] getClassData(String name) {
//pri.xiaowd.test.A --> D:/myjava/pei/xiaowd/test/A.class
String path = rootDir + "/" + name.replace('.','/') + ".class";
// System.out.println(path);
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(path);
byte[] bytes = new byte[1024];
int temp = 0;
while((temp = is.read(bytes)) != -1){
baos.write(bytes,0,temp);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
try {
if(baos != null){
baos.close();
}
if(is != null){
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
参考网址