什么是类加载器?
先了解类的加载过程:
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。他也是一个类。
一般来说,Java 虚拟机使用 Java 类的方式如下:
Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
java.lang.ClassLoader类
public abstract class ClassLoader extends Object
类加载器是负责加载类的对象。ClassLoader 类是一个抽象类。
如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。一般策略是将名称转换为某个文件名,然后从文件系统读取该名称的“类文件”。
每个 Class 对象都包含一个对定义它的 ClassLoader 的引用。
ClassLoader 双亲委托模型
ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
loadClass(String name)是ClassLoader的一个重要方法,加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
类加载器是用来把类(class)装载进内存的。JVM 规范定义了两种类型的类加载器:启动类加载器(bootstrap)和用户自定义加载器(user-defined class loader)。 JVM在运行时会产生3个类加载器组成的初始化加载器层次结构 ,如下图所示:
引导类加载器Bootstap:用C++编写的,是JVM自带的类加载器,负责Java平台核心库,用来加载核心类库。该加载器无法直接获取
扩展类加载器Extension:负责jre/lib/ext目录下的jar包或 –D java.ext.dirs 指定目录下的jar包装入工作库
系统类加载器System:负责java –classpath 或 –D java.class.path所指的目录下的类与jar包装入工作 ,是最常用的加载器
举个例子:
为了方便直接看截图
可以看出
ClassLoader cl = ClassLoader.getSystemClassLoader();
cl = Class.forName("com.ClassLoader.Test").getClassLoader();
这两种调用方法结果是一样的,
第一次打印出sun.misc.Launcher$AppClassLoader系统类加载器
第二次打印出sun.misc.Launcher$ExtClassLoader扩展类加载器
第三次打印出null
但是加载Object类
第一次打印出null
第二次出现空指针异常,因为Object类属于虚拟机的内置类加载器(称为 “bootstrap class loader”)本身没有父类加载器,但是可以将它用作 ClassLoader 实例的父类加载器。
关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流
InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream(“exer2\test.properties”);
System.out.println(in);
自定类加载器
第一步,自定义一个实体类Person.java,我们把它编译后的Person.class放在E盘根目录下。
package com.ClassLoader;
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString() {
return "I am a person, my name is " + name;
}
}
第二步,自定义一个类加载器,另外注意一下 defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class—-只要二进制字节流的内容符合Class文件规 范。我们自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只实现findClass方法:
package com.ClassLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader {
public MyClassLoader() {
}
protected Class findClass(String name) throws ClassNotFoundException {
File file = new File("E:\\Person.class");
FileInputStream fis = null;
Class c = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e1) {
e1.printStackTrace();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b = 0;
try {
while ((b = fis.read()) != -1) {
baos.write(b);
}
} catch (IOException e1) {
e1.printStackTrace();
}
try {
byte[] bytes = baos.toByteArray();
c = this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return c;
}
}
注意:1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可.2、如果想打破双亲委派模型,那么就重写整个loadClass方法.当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。
第三步,Class.forName有一个三个参数的重载方法,可以指定类加载器,平时我们使用的Class.forName(“XX.XX.XXX”)都是使用的系统类加载器Application ClassLoader。
public class TestMyClassLoader {
public static void main(String[] args) throws Exception {
MyClassLoader mcl = new MyClassLoader();
Class c1 = mcl.findClass("com.ClassLoader.Person");
Object obj = c1.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}
}
结果
I am a person, my name is null
com.ClassLoader.MyClassLoader@3918d722
这里比较容易出问题的是第二行的打印出来的是”sun.misc.Launcher$AppClassLoader”。造成这个问题的关键在于MyEclipse是自动编译的,Person.java这个类在ctrl+S保存之后或者在Person.java文件不编辑若干秒后,MyEclipse会帮我们用户自动编译Person.java,并生成到CLASSPATH也就是bin目录下。在CLASSPATH下有Person.class,那么自然是由Application ClassLoader来加载这个.class文件了。解决这个问题有两个办法:
1.删除CLASSPATH下的Person.class,CLASSPATH下没有Person.class,Application ClassLoader就把这个.class文件交给下一级用户自定义ClassLoader去加载了。
2.TestMyClassLoader类的第5行这么写“MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());”, 即把自定义ClassLoader的父加载器设置为Extension ClassLoader,这样父加载器加载不到Person.class,就交由子加载器MyClassLoader来加载了。
.class()和getClass()的区别
.class方法和getClass()二者都可以获取一个唯一的java.lang.Class对象,但是区别在于:
1、.class用于类名,getClass()是一个final native的方法,因此用于类实例
2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象
Java 虚拟机是如何判定两个 Java 类是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。