16.Java的类加载器
一.类加载器
1.概述:
- 类加载器是 Java 语言的一个创新,也是 Java 语言流行的重要原因之一。它使得 Java 类可以被动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 就出现了,最初是为了满足 Java Applet 的需要而开发出来的。Java Applet 需要从远程下载 Java 类文件到浏览器中并执行。现在类加载器在 Web 容器和 OSGi 中得到了广泛的使用。一般来说,Java 应用的开发人员不需要直接同类加载器进行交互。Java 虚拟机默认的行为就已经足够满足大多数情况的需求了。不过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试
ClassNotFoundException
和NoClassDefFoundError
等异常。 - 基本概念:当需要用到某个类时,虚拟机将会加载类的二进制文件,并在内存中创建对应的class对象,这个过程称之为类的加载
2.整个过程示意图:
- 磁盘上存放:.java文件以及经过编译的.class文件
- 将磁盘上的.class文件加载进内存,然后连接,初始化生成 Class对象
- 在内存中有方法区和堆区,在堆区的Class对象会引用方法区的二进制数据文件。
3.详细讲解加载,连接和初始化
- 加载:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构
- 在内存中生成代表此类的java.lang.Class对象,作为该类访问入口.
- 连接:分为三个阶段
- 验证:连接阶段第一步.验证的目的是确保Class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击.大致完成以下四个校验动作:
- 文件格式验证
- 源数据验证
- 字节码验证
- 符号引用验证
- 准备:连接阶段第二步,正式为类变量分配内存并设置变量的初始值.(仅包含类变量,不包含实例变量)。
- 解析:连接阶段第三步,虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等。
- 验证:连接阶段第一步.验证的目的是确保Class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击.大致完成以下四个校验动作:
- 初始化:开始执行类中定义的java程序代码,执行类构造器。
4.类加载器分类
(1)类加载器分为两种:
- Java虚拟机自带的类加载器(3种)
- 启动类加载器:Bootstrap ClassLoader,又名根类加载器或引导类加载器
- 扩展类加载器:Extendsion ClassLoader
- 系统类加载器:Application ClassLoader,又名应用类加载器
- 用户自定义的类加载器:一般是java.lang.ClassLoader的子类实例。
(2)Bootstrap ClassLoader:根类加载器
概念:最底层的类加载器,是虚拟机的一部分,由C++语言实现且没有父类加载器,也没有继承java.lang.ClassLoader类。它主要负责加载系统属性"sun.boot.class.path"指定的路径下的核心类库(jre\lib),出于安全考虑,根类加载器只加载java、javax、sun开头的类
注:由于Object类的类加载器是根类加载器,所以打印出来是null
1 public class ClassLoaderDemo1 { 2 3 public static void main(String[] args) { 4 ClassLoader classLoader = Object.class.getClassLoader(); 5 System.out.println("Object类的类加载器是:" + classLoader);//Object类的类加载器是:null 6 } 7 }
(3)Extendsion ClassLoader:扩展类加载器
概念:有原SUN公司实现的类(JDK8和JDK9实现的类名不同),是由Java语言编写,父类加载器是根类加载器。负责加载jre\lib\ext目录下的类库或者系统变量“java.ext.dirs"指定的目录下的类库。
1 public class ClassLoaderDemo1 { 2 3 public static void main(String[] args) { 4 ClassLoader classLoader = DNSNameService.class.getClassLoader(); 5 System.out.println("DNSNameService类的类加载器是:" + classLoader);//DNSNameService类的类加载器是:sun.misc.Launcher$ExtClassLoader@7ea987ac 6 } 7 }
(4)Application ClassLoader:系统类加载器
概念:有原SUN公司实现的类(JDK8和JDK9实现的类名不同),是由Java语言编写,父类加载器是扩展类加载器。负责加载从classpath环境变量或者系统属性java.class.path所指定的目录中加载类。它是用户自定义类加载器的默认父类加载器。一般情况下,该类加载器是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader()直接获得。
1 public class ClassLoaderDemo1 { 2 3 public static void main(String[] args) { 4 ClassLoader classLoader = ClassLoaderDemo1.class.getClassLoader(); 5 System.out.println("ClassLoaderDemo1类的类加载器是:" + classLoader);//ClassLoaderDemo1类的类加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2 6 } 7 }
(5)小结:
在程序开发中,Java虚拟机对class文件采用的是按需加载的方式,需要该类时才会将该类的class文件加载进内存生成class对象,加载时采用双亲委派模式,即把加载类的请求交由父类加载器处理,它是一种任务委派模式。
类加载器树状组织结构示意图:
5.类加载器的双亲委派机制
委派机制图:
测试:
由于扩展类加载器的父类加载器为根类加载器是C++实现的,则获得到的classLoader=null,所以只显示两个类加载器
1 public class ClassLoaderDemo1 { 2 3 public static void main(String[] args) { 4 ClassLoader classLoader = ClassLoaderDemo1.class.getClassLoader(); 5 6 while (classLoader != null){ 7 System.out.println(classLoader); 8 classLoader = classLoader.getParent();//获取父类加载器 9 } 10 11 /**结果: 12 * sun.misc.Launcher$AppClassLoader@18b4aac2 13 * sun.misc.Launcher$ExtClassLoader@6d6f6e28 14 * 15 */ 16 } 17 }
注:自定义的类包名不要是java、javax、sun开头的类,这样获得的类加载器会报错
6.ClassLoader
- 概念:所有的类加载器(除了根类加载器)都必须继承java.lang.ClassLoader,它是一个抽象类。
- 主要方法:
- protected Class<?> loadClass(String name, boolean resolve) :加载Class对象
- 注意:不要覆盖该方法
- 步骤:
- 通过二进制名称调用 findLoadedClass 方法检查是否已经加载该类
- 没有加载则在父类加载器上调用loadClass方法,如果父类加载器为null,则使用根类加载器
- 调用 findClass 方法查找该类
- protected Class<?> findClass(String name) :在自定义类时我们需要覆盖这个类
- :用byte字节解析成虚拟机能够是别的Class对象。
- 通常与findClass方法一起使用。
- 在自定义类加载器是,会直接覆盖ClassLoader的findClass方法获取需要加载的类的字节码,最后调用defindClass方法生成Class对象。
- protected final void resolveClass(Class<?> c) :指定连接的类。类加载器可以使用此方法来连接类。
- protected Class<?> loadClass(String name, boolean resolve) :加载Class对象
7.URLClassLoader
- 在java.net包中,JDK提供了一个更加易于使用的类加载器URLClassLoader,来扩展ClassLoader,能够从本地或网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用
(1)主要方法:
- URLClassLoader(URL[] urls) :指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。
- URLClassLoader(URL[] urls, ClassLoader parent) :指定要加载类的URL地址,并指定父类加载器。
加载磁盘上的类:
public class Demo02 { public Demo02(){ System.out.println("Demo02 初始化"); } }
import java.io.File; import java.net.URL; import java.net.URLClassLoader; public class Demo01 { public static void main(String[] args) throws Exception { //需要先将需要加载的类生成class文件 //先定位到要加载的类的加载器 File file = new File("code.Demo02"); URL url = file.toURI().toURL(); URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}); System.out.println("父类加载器:"+urlClassLoader.getParent()); //再用加载器加载类 Class cls = urlClassLoader.loadClass("code.Demo02"); cls.newInstance(); //该类必须要有一个无参构造函数 } /* 结果: 父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2 Demo02 初始化 * */ }
加载网络上的类:
8.自定义类加载器
-
自定义类加载器,只需要继承ClassLoader类,并覆盖掉findClass方法即可
(1)自定义磁盘文件类加载器
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; public class MyFileClassLoader extends ClassLoader { private String direcory; //被加载类所在的目录 public MyFileClassLoader(String direcory) { this.direcory = direcory; } public MyFileClassLoader(String direcory, ClassLoader parent) { super(parent); this.direcory = direcory; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { //把类名转换为目录 String file = direcory + File.separator + name.replace(".", File.separator) + ".class"; System.out.println("重写了findClass方法"); //构建输入流 InputStream in = new FileInputStream(file); //构建字节输出流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte buf[] = new byte[1024]; int len = -1; while ((len = in.read(buf)) != -1) { baos.write(buf, 0, len); } //读取到的字节码的二进制数据 byte data[] = baos.toByteArray(); in.close(); baos.close(); return defineClass(name, data, 0, data.length); } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) throws Exception { //获得项目的当前路径 MyFileClassLoader classLoader = new MyFileClassLoader("F:\\java_code\\1.Java_Base\\common_java\\out\\production\\common_java"); Class<?> cls = classLoader.findClass("code.Demo02"); cls.newInstance(); } }
(2)自定义网络上的类的类加载器
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; public class MyURLClassLoader extends ClassLoader { private String url; //被加载类所在的目录 public MyURLClassLoader(String url) { this.url = url; } public MyURLClassLoader(String url, ClassLoader parent) { super(parent); this.url = url; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { //把类名转换为路径 String path = url + "/" + name.replace(".", "/") + ".class"; //构建输入流 URL url = new URL(path); InputStream in = url.openStream(); //构建字节输出流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte buf[] = new byte[1024]; int len = -1; while ((len = in.read(buf)) != -1) { baos.write(buf, 0, len); } //读取到的字节码的二进制数据 byte data[] = baos.toByteArray(); in.close(); baos.close(); return defineClass(name, data, 0, data.length); } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) throws Exception { //获得网络上的项目 MyURLClassLoader classLoader = new MyURLClassLoader("http://localhost:8080/examples"); Class<?> cls = classLoader.findClass("code.Demo02"); cls.newInstance(); } }
9.热部署类加载器
-
相同的类被同一个类加载器多次加载,则会报错。因此热部署是让同一个类文件被不同的类加载器加载重复加载即可。
-
我们不能调用loadClass方法,而应该调用findClass方法,避免双亲委派机制,从而实现同一个类被加载多次,实现热部署
此代码我们可知classLoader1的父类加载器为应用类加载器,classLoader2的父类加载器为classLoader1
我们先测试使用loadClass发现两个hashCode是一样的,这是因为都是用的是应用类加载器的loadClass产生的
然后我们该用findClass发现两个hashCode不一样,这是因为classLoader1使用自定义的findClass自己找到后就加载了,classLoader2同样也是
我们实现了一个类被加载两次,实现了热部署类加载器,只需重写findClass方法
public class ClassLoaderDemo03 { public static void main(String[] args) throws Exception { MyFileClassLoader classLoader1 = new MyFileClassLoader("F:\\java_code\\1.Java_Base\\common_java\\out\\production\\common_java"); MyFileClassLoader classLoader2 = new MyFileClassLoader("F:\\java_code\\1.Java_Base\\common_java\\out\\production\\common_java", classLoader1); //获得两个实例的父类加载器 System.out.println(classLoader1.getParent()); System.out.println(classLoader2.getParent()); // Class cls1 = classLoader1.findClass("code.Demo02"); // Class cls2 = classLoader2.findClass("code.Demo02"); Class cls3 = classLoader1.loadClass("code.Demo02"); Class cls4 = classLoader2.loadClass("code.Demo02"); // System.out.println(cls1.hashCode()); // System.out.println(cls2.hashCode()); System.out.println(cls3.hashCode()); System.out.println(cls4.hashCode()); } }
10.类的显示加载和隐式加载
- 显示加载:在代码中调用ClassLoader加载class对象,例如:Class.forName(String name)方法或this.getClass().getClassLoader().loadClass(String name)方法加载类
- 隐式加载:没有明确调用加载的代码,而加再进内存中。例如:对象创建时或在其他类加载器加载时而引入别的类加载器
11.线程上下文类加载器
-
虽然违背了双亲委派模式但是使用更加灵活了。