简介
类加载器负责将class文件读入内存, 系统为载入内存的类创建对应的Class对象。类加载器通常由JVM提供, JVM提供的加载器通常被称为系统类加载器。此外, 开发者可以通过继承ClassLoader来实现自己的类加载器。
思维导图
类加载器
类加载器的层次结构:
- Bootstrap ClassLoader: 根类加载器
- Extension ClassLoader: 扩展类加载器
- System ClassLoader: 应用程序类加载器(系统类加载器)
根类加载器负责加载核心类库。Java存放在%JAVA_HONE%/jre/lib目录中的, 或者被-XbootClasspath参数所指定的路径中的类。 它并不是java.lang.ClassLoader的子类, 是由JVM自身实现的。
public class BootstrapTest {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
/**
* 输出:
*
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/resources.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/rt.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/sunrsasign.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jsse.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jce.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/charsets.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/lib/jfr.jar
* file:/C:/Program%20Files/Java/jdk1.7.0_80/jre/classe
**/
可以看到, 常用到的rt.jar就是被根加载器加载的。
扩展类加载器负责加载JRE扩展目录(%JAVA_HONE%/jre/lib/ext或者由java.ext.dirs系统指定的目录)中jar包中的类。
应用程序类加载器(系统类加载器) 负责加载用户类路径(Class Path)上所指定的类库:
-来自java命令的-classpath选项
-java.class.path系统属性
-CLASSPATH环境变量
如果程序中没有定义自己的类加载器, 一般情况下系统类加载器就是程序中的默认的类加载器。如果没有特别指定, 用户自定义的类加载器都以系统类加载器作为父加载器。
public static void main(String[] args) throws IOException {
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载器:" + systemLoader);
// 获取系统类加载器的加载路径
Enumeration<URL> eml = systemLoader.getResources("");
while(eml.hasMoreElements()){
System.out.println(eml.nextElement());
}
//获取扩展类加载器
ClassLoader extendsionLoader = systemLoader.getParent();
System.out.println("扩展类加载器:" + extendsionLoader);
System.out.println("扩展类加载器的加载路径: " + System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent:" + extendsionLoader.getParent());
}
/**
* 输出:
*
* 系统类加载器:sun.misc.Launcher$AppClassLoader@164dbd5
* file:/C:/Users/peter1950/Desktop/T/
* file:/C:/Program%20Files/Java/jdk1.7.0_80/lib/
* 扩展类加载器:sun.misc.Launcher$ExtClassLoader@9cb0f4
* 扩展类加载器的加载路径: C:\Program Files\Java\jdk1.7.0_80\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
* 扩展类加载器的parent:null
**/
系统类加载器加载的路径为当前目录下的路径, 以及%JAVA_HONE%/jre/lib目录, 这正是ClASSPATH环境变量的值。
扩展类加载器加载的路径为ext目录下的类。可以看到, 扩展类加载器的父类加载器为null, 实际上扩展类加载器的父类就是根类加载器(Bootstrap ClassLoader), 因为根类加载器没有继承ClassLoader, 是由JVM自身实现的, 因此返回null.
类加载器的关系一般如图所示——
上图中的层次关系被称为双亲委派模型, 双亲委派模型要求:除了根加载器之外, 所有的加载器都要有父加载器。 类加载器的父子关系并非继承上的父子关系, 而是通过使用组合关系来复用父类加载器的代码。
// 双亲委派模型实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先, 先检查这个类是否被加载过了
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 调用父类加载器进行加载
c = parent.loadClass(name, false);
} else {
// 父类加载器是根类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 父类加载器加载失败
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父类加载器加载失败
// 调用自身的findclass方法进行加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
加载策略
类加载机制主要有3种:
- 全盘负责。指类加载器不仅负责当前class的加载, 而且也要负责当前class所依赖的class的加载。
- 双亲委托。就是在加载时先让父类加载器先尝试加载, 当父类加载器无法加载时子加载器才会进行加载。
- 缓存机制。缓存机制保证所有的加载过的Class都会被缓存。当程需要使用某个Class时, 会先从缓存中搜索。当缓存中不存在Class时, 系统才会读取该类的二进制数据加载。
自定义类加载器
主要有2种方法:
(1) 重写loadClass方法
(2) 重写findClass方法
推荐重写findClass方法, 因为重写findClass方法可以避免覆盖默认类加载器的父类委托、缓存机制2种策略。
// ——代码参考:《疯狂Java讲义》
public class CompileClassLoader extends ClassLoader {
// 读取一个文件的内容
private byte[] getBytes(String filename) throws IOException {
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fin = new FileInputStream(file);) {
int r = fin.read(raw);
if (r != len) {
throw new IOException("无法读取全部文件:" + "r" + r + "!=" + len);
}
return raw;
}
}
// 定义编译指定java文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader正在编译" + javaFile + "...");
Process p = Runtime.getRuntime().exec("javac -encoding utf-8 " + javaFile);
try {
// 其它线程都等待这个线程完成
p.waitFor();
} catch (InterruptedException e) {
System.out.println(e);
}
// 获取javac 线程退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// java源文件存在 && (class文件还未存在 或者 java源文件修改的时间比class文件生成的时间晚)
if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())) {
try {
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (classFile.exists()) {
try {
// 将class二进制文件读入数组
byte[] raw = getBytes(classFilename);
// 获取类的全限定名(包名.类名)
String[] paths = name.split("java");
String entireName = new StringBuilder(paths[1]).delete(0,1).toString().
replaceAll("\\\\", "\\.");
// 调用ClassLoader的defineClass方法将二进制数据转化成Class对象
clazz = defineClass(entireName, raw, 0, raw.length);
} catch (IOException e) {
e.printStackTrace();
}
}
// 加载失败
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 运行时缺少参数
if (args.length < 1) {
System.out.println("缺少目标类, 请按如下格式运行java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数时需要运行的类
String progClass = args[0];
// 剩余参数作为运行目标类时的参数
// 将这些参数放到新数组中。
String[] proArgs = new String[args.length - 1];
System.arraycopy(args, 1, proArgs, 0, proArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
Method main = clazz.getMethod("main", (new String[0].getClass()));
Object argsArray[] = {proArgs};
main.invoke(null, argsArray);
}
}
// 运行命令:java Hello ni hao
// 运行结果:
运行hello的参数: ni
运行hello的参数: hao
// 加载目标类
public class Hello {
public static void main(String[] args) {
for (String s : args) {
System.out.println("运行hello的参数: " + s);
}
}
}