1、什么是类加载器?
简单说,类加载器就是加载类的Java工具类。
2、Java系统默认的3个类加载器:
JVM中可以安装多个类加载器,系统默认的主要有3个,并且每一个类加载器都负载加载不同位置的类。
Bootstrap ClassLoader:此类加载器采用C++编写,内嵌在JVM内核当中,一般开发中是看不到的。它负责加载的是jre\lib\rt.jar中的类。(注意:Bootstrap类加载器不是一个Java类,它是第一个类加载器,可以加载本身也是java类的类加载器。)
Extension ClassLoader:用来进行扩展类的加载,一般负责加载jre\lib\ext目录中的类。
AppClassLoader:加载classpath指定的类,是最常使用的一种类加载器。
代码示例:查看一个类所使用的类加载器
packagecn.itcast.day2;
public class ClassLoadTest {
public static void main(String[]args) {
System.out.println(ClassLoadTest.class.getClassLoader().getClass().getName());
//结果是:sun.misc.Launcher$AppClassLoader
System.out.println(System.class.getClassLoader());
//System类是Bootstrap类加载器加载的,返回null。
}
}
3、类加载器之间的父子关系和负载加载范围图
代码示例:查看java中类加载器的树形父子关系结构
packagecn.itcast.day2;
public class ClassLoadTest {
public static void main(String[]args) {
ClassLoader loader = ClassLoadTest.class.getClassLoader();
while(loader != null){
System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
System.out.println(loader);
}
}
输出结果:
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null
4、演示使用Eclipse生成某个类的jar包并放在特定类加载负责的目录下,判断由哪个类加载器加载:
需求:将ClassLoaderTest这个类的class文件打成jar包存放在jre\lib\ext\这个目录下,并查看该类由哪个类加载器加载。
操作步骤:
在Eclipse中右键点击ClassLoaderTest这个类,选择导出Export
展开JAVA目录,选择“JAR file”,然后点击“Next”
依次展开ClassLoaderTest这个类的源文件路径,点击Browse,选择生成的jar包导出之后的存放目录
最后点击Finish完成jar包导出。
接下来再运行ClassLoaderTest这个类,输出结果如下图所示:
从运行结果中可以发现现在ClassLoaderTest这个类现在由ExtClassLoader类加载器加载。
5、类加载器的委托机制
思考问题:当JVM要加载一个类时,到底是派哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,JVM将使用类A的类加载器去加载类B的类加载器。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载指定的类。
每个类加载器加载类时,先是委托给其上级类加载器。当所有的上级类加载器没有加载到类时,又回到发起者类加载器,如果还是加载不了,则会抛出ClassNotFoundException异常,而不是再去找发起者类加载器的子类加载器。因为没有getChild()方法,即使是有这个方法,可是也可能有多个子类加载器,不清楚到底选哪一个。
委托机制的详细说明:
每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的委托模式。类装载器一级一级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级一级回退到子孙类加载器去进行真正的加载。当回退到最初的类加载器时,如果它自己也不能完成类的装载,那就抛出ClassNotFoundException异常警告。
委托机制理解说明图解:
面试题:能不能自己写个类加载器加载java.lang.System类?
答:不能,即使写了也不会被类加载器加载。为了不让用户写System类,类加载器采用委托机制,这样可以保证父级类加载器优先加载,也就是如果父级类加载器可以加载,就由父级类加载器加载。而System类被委托到Bootstrap类加载器这一级时就直接被它给加载了,而不会使用用户自定义的类加载器加载。
6、自定义类加载器的编写原理分析
注意:自定义的类加载器必须要继承ClassLoad类。
自定义类加载器编写原理:ClassLoad类中的loadClass()方法内部实现了父类委托机制,因此我们没有必要覆写laodClass()方法,否则就需要自己去实现父类的委托机制。我们只需要覆写父类中的findClass()方法,且loadClass()方法中调用了findClass()方法,使用的则是“模版设计模式”,当得到Class文件之后,就可以通过defineClass()方法将二进制数据转换成字节码。
JDK API文档提供的代码示例:
需求:编写对class文件进行加密和解密的工具类验证自定义类加载器。
编程步骤:
编写一个对文件内容进行简单加密的程序。
编写一个自定义的类装载器,可以实现对加密过的类进行加载和解密。
实验步骤:
1) 对不带包名的class文件进行加密,加密结果存放到另外一个目录。例如: java MyClassLoader MyTest.class F:\itcast。
2) 运行加载类的程序,结果能够被正常加载,但打印出来的类装载器名称为AppClassLoader:java MyClassLoader MyTest F:\itcast。
3) 用加密后的类文件替换classpath路径下的类文件,再执行上一步操作就出问题了,错误说明是AppClassLoader类加载器加载失败了。
代码示例:
MyClassLoader.java
package cn.itcast.day2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader {
public static void main(String[] args)throws Exception {
String srcPath = args[0];
String destDir = args[1];
FileInputStream fis = new FileInputStream(srcPath);
String destFileName = srcPath.substring(srcPath.lastIndexOf('\\')+1);
String destPath = destDir+"\\"+destFileName;
FileOutputStream fos = new FileOutputStream(destPath);
cypher(fis, fos);
fis.close();
fos.close();
}
//对输入流数据进行加密的方法。
private static void cypher(InputStream ips,OutputStream ops)throws Exception{
int b = -1;
while((b=ips.read())!=-1){
ops.write(b ^ 0xff);
}
}
}
ClassLoadAttachment.java
package cn.itcast.day2;
import java.util.Date;
public class ClassLoadAttachment extends Date {
public String toString(){
return "helloitcast";
}
}
ClassLoadTest.java
package cn.itcast.day2;
public class ClassLoadTest {
public static void main(String[] args) {
System.out.println(new ClassLoadAttachment().toString());
}
}
首先执行MyClassLoader类中的main方法,在执行之前先设置传入的参数:在MyClassLoader类的代码编辑区域中右键点击,选择Run As → Run Configurations
在运行的时候一定要确保main方法所在的类是否是MyClassLoader类,然后再点击Run运行。
执行完了之后,选中左边文件列表栏中的itcastlib文件夹,按F5刷新一下,就可以看到被加密编码之后生成的字节码文件ClassLoadAttachment.class
接下来,将此itcastlib目录下的ClassLoadAttachment.class这个类文件覆盖掉当前项目中的\bin\cn\itcast\day2目录下的ClassLoadAttachment.class文件。
覆盖完成之后,重新执行ClassLoadTest中的main方法,这时就会发现报错。原因是此时AppClassLoader类加载器已经无法正确加载加密编码之后的class文件了,需要重新编写一个具有解密功能的类加载器进行加载才可以。报错提示信息如下:
Exception in thread"main" java.lang.ClassFormatError: Incompatible magic value 889275713in class file cn/itcast/day2/ClassLoadAttachment
atjava.lang.ClassLoader.defineClass1(Native Method)
atjava.lang.ClassLoader.defineClass(ClassLoader.java:760)
atjava.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
atjava.net.URLClassLoader.defineClass(URLClassLoader.java:455)
atjava.net.URLClassLoader.access$100(URLClassLoader.java:73)
atjava.net.URLClassLoader$1.run(URLClassLoader.java:367)
atjava.net.URLClassLoader$1.run(URLClassLoader.java:361)
atjava.security.AccessController.doPrivileged(Native Method)
atjava.net.URLClassLoader.findClass(URLClassLoader.java:360)
atjava.lang.ClassLoader.loadClass(ClassLoader.java:424)
atsun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
atjava.lang.ClassLoader.loadClass(ClassLoader.java:357)
atcn.itcast.day2.ClassLoadTest.main(ClassLoadTest.java:21)
编写并测试自定义的解密类加载器
代码示例:
MyClassLoader.java
package cn.itcast.day2;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
public static void main(String[] args) throws Exception {
private String classDir;
public MyClassLoader(){}
public MyClassLoader(String classDir){
this.classDir = classDir;
}
@Override
protected Class> findClass(String name) throwsClassNotFoundException {
String classFileName = classDir + "\\" + name + ".class";
try {
FileInputStream fis = new FileInputStream(classFileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
cypher(fis, bos);
fis.close();
byte[] bytes = bos.toByteArray();
defineClass(bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private static void cypher(InputStream ips,OutputStream fos) throws Exception{
int b = -1;
while((b = ips.read()) != -1){
fos.write(b ^ 0xff);
}
}
}
ClassLoaderTest.java
package cn.itcast.day2;
import java.util.Date;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
Class clazz = new MyClassLoader("itcastlib").loadClass("ClassLoaderAttachment");
Date d1 = (Date)clazz.newInstance();
System.out.println(d1); //输出结果:hello itcast
}
}
注意:
1、之所以让ClassLoaderAttachment类继承Date是因为,如果直接写ClassLoaderAttachment d1 =(ClassLoaderAttachment)clazz.newInstance();这条语句,编译的时候就会报错,因为此时的ClassLoaderAttachment在AppClassLoader加载进内存后就无法识别。所以需要通过借助一个父类对象绕过编译器。也就是:Date d1 = (Date)clazz.newInstance();。
2、如果想让父类加载器AppClassLoader加载ClassLoaderAttachment类,则需要执行下面的语句:
Class clazz = newMyClassLoader("itheimalib" ).loadClass("com.itheima.day2.ClassLoaderAttachment");
7、类加载器的一个高级问题的实验分析:
①、编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为WebAppClassLoader
②、把MyServlet.class文件打成jar包,放到jdk的\jre\lib\ext目录中,然后重启Tomcat服务器,出现找不到HttpServlet的错误。
③、把Tomcat的\lib目录下的tomcat-api.jar包也拷贝到\jre\lib\ext目录中之后,问题就解决了,打印的结果是ExtClassLoader
实验步骤:
①、新建一个web project工程。
②、新建完成之后,在刚刚建好的web工程里面,右键点击src → New → Servlet → 输入包名、类名、选择需要创建的doGet方法 → Next → Finish完成。
③、修改MyServlet.java类中的doGet方法的内容,代码如下:
package cn.itcast.itcastweb.web.servlets;
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 MyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponseresponse) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
ClassLoader loader = this.getClass().getClassLoader();
while(loader != null){
out.println(loader.getClass().getName()+"
");
loader = loader.getParent();
}
out.close();
}
}
④、以上步骤就相当于完成了该web项目,接下来需要将该已经做好的web项目放到Tomcat服务器里面去,也就是需要部署该web项目。
点击Finish之后,点击OK完成部署过程。
记住,完成以上配置之后还需要配置一下Tomcat服务器和当前系统的JDK版本(我当前的JDK版本是jdk1.8.0_05)的关系设置,步骤如下:
找到Tomcat的bin目录下的startup.bat,右键选中该文件点击“编辑”,在其中添加一条配置信息,如下图所示:
添加配置信息之后,保存即可。
⑤、完成以上部署和配置之后,接下来启动Tomcat服务器,双击Tomcat的bin目录中的startup.bat启动Tomcat服务器。
启动完Tomcat服务器之后,在浏览器里面输入访问MyServlet的地址http://localhost:8080/itcastweb2/servlet/MyServlet,然后打开该页面,显示如下图所示:
可以看到MyServlet是由WebAppClassLoader类加载器加载的。
⑥、接下来,将MyServlet导成jar包,导出到\jre\lib\ext目录下,jar包名称为myservlet.jar,步骤如下图所示:
⑦、导出jar包完成之后,重启Tomcat服务器,然后再次在浏览器中访问MyServlet的地址http://localhost:8080/itcastweb2/servlet/MyServlet,会发现出现提示错误无法加载HttpServlet,如下图所示:
出错原因:
在没有将MyServlet导出为jar包到jre\lib\ext目录之前,MyServlet类以及其父类HttpServlet都是由WebAppClassLoader类加载器加载的,但是当导出到jre\lib\ext目录之后,MyServlet类就由ExtClassLoader类加载器加载,但是HttpServlet却不在jre\lib\ext目录下,所以无法加载到,就出现了以上图中的报错。
而且由于MyServlet和HttpServlet是同一线程的,这也就意味着它们是由同一个类加载器加载的,ExtClassLoader类加载器加载不了,就会直接报错,而不会再交给WebAppClassLoader类加载器加载。
总结一句话:父级类加载器加载的类无法引用只能被子级类加载器加载的类。原理如下图所示:
解决方法:
将HttpServlet所在的jar(Tomcat所在的目录的lib目录下的servlet-api.jar)包拷贝到jre\lib\ext目录下,然后重新启动Tomcat服务器,之后在浏览器中重新访问MyServlet的地址。
此时可以看到加载MyServlet的类加载器为ExtClassLoader类加载器。如下图所示: