java之ClassLoader源码分析

类加载器ClassLoader的含义

不论多么简单的java程序,都是由一个或者多个java文件组成,java内部实现了程序所需要的功能逻辑,类之间可能还存在着依赖关系。当程序运行的时候,类加载器会把一部分类编译为class后加载到内存中,这样程序才能够调用里面的方法并运行。


类之间如果存在依赖关系,那么类加载会去帮你加载相关的类到内存中,这样才能够完成调用。如果找不到相关的类,那么他就会抛出我们在开发经常见到的异常:ClassNotFoundException


Java中的所有类,必须被装载到jvm中才能运行,这个装载工作是由jvm中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,与此同时在loadClass中存在着三种加载策略,loadClass使用双亲委派模式


所以Classloader就是用来动态加载Class文件到内存当中用的。

Java默认提供的三个ClassLoader

1.Bootstrap ClassLoader

称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,预设上它负责搜寻JRE所在目录的classes或lib目录下(实际上是由系统参数sun.boot.class.path指定)。如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
> 这两种输出的内容都是一样的。
System.out.println(System.getProperty("sun.boot.class.path"));

输出

file:/F:/JDK/jdk1.8/jre/lib/resources.jar
file:/F:/JDK/jdk1.8/jre/lib/rt.jar
file:/F:/JDK/jdk1.8/jre/lib/sunrsasign.jar
file:/F:/JDK/jdk1.8/jre/lib/jsse.jar
file:/F:/JDK/jdk1.8/jre/lib/jce.jar
file:/F:/JDK/jdk1.8/jre/lib/charsets.jar
file:/F:/JDK/jdk1.8/jre/lib/jfr.jar
file:/F:/JDK/jdk1.8/jre/classes
F:\JDK\jdk1.8\jre\lib\resources.jar;F:\JDK\jdk1.8\jre\lib\rt.jar;F:\JDK\jdk1.8\jre\lib\sunrsasign.jar;F:\JDK\jdk1.8\jre\lib\jsse.jar;F:\JDK\jdk1.8\jre\lib\jce.jar;F:\JDK\jdk1.8\jre\lib\charsets.jar;F:\JDK\jdk1.8\jre\lib\jfr.jar;F:\JDK\jdk1.8\jre\classes

2.Extension ClassLoader

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar(实际上是由系统参数java.ext.dirs指定)

3.App ClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件(由系统参数java.class.path指定)

总结

Extension ClassLoader和App ClassLoader 这两个类加载器实际上都是继承了ClassLoader类,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器,也就是说:

Bootstrap Loader会在JVM启动之后载入,之后它会载入ExtClassLoader并将ExtClassLoader的parent设为Bootstrap Loader,然后BootstrapLoader再加载AppClassLoader,并将AppClassLoader的parent设定为 ExtClassLoader。

ClassLoader加载类的原理

双亲委托加载模式

我们知道除了顶级的 Bootstrap Loader他的parent属性为null之外,其他的两个或者自定义的类加载器都是存在parent 的。


当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象
类加载顺序

为什么要使用双亲委托这种模型呢

java是一门具有很高的安全性的语言,使用这种加载策略的目的是为了:防止重载,父类如果已经找到了需要的类并加载到了内存,那么子类加载器就不需要再去加载该类。安全性问题。两个原因

JVM在搜索类的时候,又是如何判定两个class是相同的

  • 类名是否相同
  • 否由同一个类加载器实例加载

看一个例子

public class TestClassLoader
{
public static void main(String[] args)
{
ClassLoader loader = TestClassLoader.class.getClassLoader(); //获得加载ClassLoaderTest.class这个类的类加载器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //获得父类加载器的引用
}
System.out.println(loader);
}
}

输出

sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null

结论

第一行结果说明:ClassLoaderTest的类加载器是AppClassLoader。

第二行结果说明:AppClassLoader的类加器是ExtClassLoader,即parent=ExtClassLoader。

第三行结果说明:ExtClassLoader的类加器是Bootstrap ClassLoader,因为Bootstrap ClassLoader不是一个普通的Java类,所以ExtClassLoader的parent=null,所以第三行的打印结果为null就是这个原因。

测试2

将ClassLoaderTest.class打包成ClassLoaderTest.jar,放到Extension ClassLoader的加载目录下(JAVA_HOME/jre/lib/ext),然后重新运行这个程序,得到的结果会是什么样呢?


输出

sun.misc.Launcher$ExtClassLoader@7852e922
null

结果分析:

为什么第一行的结果是ExtClassLoader呢?

因为ClassLoader的委托模型机制,当我们要用ClassLoaderTest.class这个类的时候,AppClassLoader在试图加载之前,先委托给Bootstrcp ClassLoader,Bootstracp ClassLoader发现自己没找到,它就告诉ExtClassLoader,兄弟,我这里没有这个类,你去加载看看,然后Extension ClassLoader拿着这个类去它指定的类路径(JAVA_HOME/jre/lib/ext)试图加载,唉,它发现在ClassLoaderTest.jar这样一个文件中包含ClassLoaderTest.class这样的一个文件,然后它把找到的这个类加载到内存当中,并生成这个类的Class实例对象,最后把这个实例返回。所以ClassLoaderTest.class的类加载器是ExtClassLoader。

第二行的结果为null,是因为ExtClassLoader的父类加载器是Bootstrap ClassLoader。

测试3:

用Bootstrcp ClassLoader来加载ClassLoaderTest.class,有两种方式:
1、在jvm中添加-Xbootclasspath参数,指定Bootstrcp ClassLoader加载类的路径,并追加我们自已的jar(ClassTestLoader.jar)
2、将class文件放到JAVA_HOME/jre/classes/目录下(上面有提到)(将ClassLoaderTest.jar解压后,放到JAVA_HOME/jre/classes目录下,如下图所示:)
提示:jre目录下默认没有classes目录,需要自己手动创建一个
提供者:Java团长

自定义ClassLoader

前言

实现自定义类加载的目的是,假设我们的类他不是存在特定的位置,可能是某个磁盘或者某个远程服务器上面,那么我们就需要自定义类加载器去加载这些类。

  • 继承继承java.lang.ClassLoader

  • 重写父类的findClass方法

  • 在findClass()方法中调用defineClass()。

这个方法在编写自定义classloader的时候非常重要,它能将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常

注意: 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

为什么不去重定义loadClass方法呢?其实也可以,但是loadClass方法内部已经实现了搜索类的策略。除非你是非常熟悉否则还是不建议这样去做。这里建议重载findClass方法,因为在loadClass中最后会去调用findClass方法去加载类。而且这个方法内部默认是空的。

分析loadClass方法

源码

/**
*
A class loader is an object that is responsible for loading classes. The
class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to
locate or generate data that constitutes a definition for the class. A
typical strategy is to transform the name into a file name and then read a
"class file" of that name from a file system.
**/

大致意思如下:

class loader是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,class loader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。


protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{
// 首先检查该name指定的class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
}else{
//parent为null,则调用BootstrapClassLoader进行加载
c = findBootstrapClass0(name);
}
}catch(ClassNotFoundException e) {
//如果仍然无法加载成功,则调用自身的findClass进行加载
c = findClass(name); //
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

自定义类加载器

package com.kingge.com;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class PersonalClassLoader extends ClassLoader{
private String rootUrl;
public PersonalClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;//this.findLoadedClass(name); // 父类已加载
//if (clazz == null) { //检查该类是否已被加载过
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
if (classData == null) {
throw new ClassNotFoundException();
}
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
//ClassLoader内置方法
/* * Converts an array of bytes into an instance of class
* Before the <tt>Class</tt> can be used it must be resolved.*/
//}
return clazz;
}
private byte[] getClassData(String name) {
InputStream is = null;
try {
String path = classNameToPath(name);
URL url = new URL(path);
byte[] buff = new byte[1024*4];
int len = -1;
is = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = is.read(buff)) != -1) {
baos.write(buff,0,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "/" + name.replace(".", "/") + ".class";
}
}

测试类:

package com.kingge.com;
public class ClassLoaderTest
{
public static void main(String[] args) {
try {
/*ClassLoader loader = ClassLoaderTest.class.getClassLoader(); //获得ClassLoaderTest这个类的类加载器
while(loader != null) {
System.out.println(loader);
loader = loader.getParent(); //获得父加载器的引用
}
System.out.println(loader);*/
String rootUrl = "http://localhost:8080/console/res";
PersonalClassLoader networkClassLoader = new PersonalClassLoader(rootUrl);
String classname = "HelloWorld";
Class clazz = networkClassLoader.loadClass(classname);
System.out.println(clazz.getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}

输出:

com.kingge.com.PersonalClassLoader@65b54208

其中HelloWorld.class文件的位置在于:
HelloWorld类

其实很多服务器都自定义了类加载器

用于加载web应用指定目录下的类库(jar或class),如:Weblogic、Jboss、tomcat等,下面我以Tomcat为例,展示该web容器都定义了哪些个类加载器:

下面以tomcat为例子

  • 1、新建一个web工程httpweb
  • 2、新建一个ClassLoaderServletTest,用于打印web容器中的ClassLoader层次结构

一下代码来自网上:

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ClassLoaderServletTest extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
ClassLoader loader = this.getClass().getClassLoader();
while(loader != null) {
out.write(loader.getClass().getName()+"<br/>");
loader = loader.getParent();
}
out.write(String.valueOf(loader));
out.flush();
out.close();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}


  • 3、配置Servlet,并启动服务

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>ClassLoaderServletTest</servlet-name>
<servlet-class>ClassLoaderServletTest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ClassLoaderServletTest</servlet-name>
<url-pattern>/servlet/ClassLoaderServletTest</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

运行截图:

运行结果图

总结

这种自定义的方式目的就是为了,能够控制类的加载流程,那么这种远程加载类的方式类似于我们常用的Hessian 来访问多个系统获取类

好的网站

http://blog.csdn.net/briblue/article/details/54973413

如果你感觉文章对你又些许感悟,你可以支持我!!
-------------本文结束感谢您的阅读-------------