一、类加载器
1、类加载器概述
在开发中会遇到 java.lang.ClassNotFoundException 和 java.lang.NoClassDefError,想要更好解决这类问题,或者在一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其加载机制成为了Java开发必备技能之一。
2、类加载器
类加载的作用: 完成类的加载。将class文件字节码内容加载到内存中, 并将这些静态数据转换成方法区的运行时数据结构, 然后在堆中生成一个代表这个类的java.lang.Class对象, 作为方法区中类数据的访问入口。
类缓存: 标准的JavaSE类加载器可以按要求查找类, 但一旦某个类被加载到类加载器中, 它将维持加载(缓存) 一段时间。 不过JVM垃圾回收机制可以回收这些Class对象。
3、类加载器作用
(1)本质工作: 类加载器的本质工作就是用于加载类;
(2)类缓存:加载到 JVM 中的类会缓存一段时间;
(3)加载文件:类加载器还可以用来加载“类路径下”的资源文件。
二、类加载器的分类
1、分类
类加载器作用是用来把类(class)装载进内存的。 JVM 规范定义了如下类型的类的加载器。
2、引导类加载器(Bootstrap Classloader),又称为根类加载器
它负责加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 等或 sun.boot.class.path 路径下的内容),是用原生代码(C/C++)来实现的,并不继承自 java.lang.ClassLoader,所以通过 Java 代码获取引导类加载器对象将会得到 null。(只有核心类库如 String 才使用 引导类加载器)
3、扩展类加载器(Extension Classloader)
它由 sun.misc.Launcher$ExtClassLoader 实现,是 java.lang.ClassLoader 的子类,负责加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)
4、应用程序类加载器(Application Classloader)
它由 sun.misc.Lanuncher$AppClassLoader 实现,是 java.lang.ClassLoader 的子类,负责加载 Java 应用程序类路径(classpath、java.class.path)下的内容。(通俗的讲:项目的路径bin文件夹下的字节码,以及如果你配置了环境变量classpath)
5、自定义类加载器
开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求,例如对字节码进行加密来避免class文件被反编译,或者加载特殊目录下的字节码数据。
6、
三、经典委托模式
1、Java 中类加载器的双亲委托模式
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。
那么,怎么样算是“同一个类”呢?
在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。换句话说,同一个类如果用两个类加载器分别加载,JVM将视为“不同的类”,它们互不兼容。
那么,我们的类加载器在执行类加载任务的时候,如何确保一个类的全局唯一性呢?
Java虚拟机的设计者们通过一种称之为“双亲委派模型(Parent Delegation Model)”的委派机制来约定类加载器的加载机制。
按照双亲委派模型的规则,除了引导类加载器之外,程序中的每一个类加载器都应该拥有一个超类加载器,比如:ExtClassLoader的超类加载器是引导类加载器,而AppClassLoader的超类加载器是ExtClassLoader,而自定义类加载器的超类就是AppClassLoader。
那么当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,先检测此类是否加载过,即在方法区寻找该类对应的Class对象是否存在,如果存在就是已经加载过了,直接返回该Class对象,否则会将加载任务委派给它的超类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止,如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载,如果所有的类加载器都加载失败,就会报java.lang.ClassNotFoundException或java.lang.NoClassDefFoundError。
在此大家需要注意,由于Java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委托模式,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器就接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
说明:
数组类型本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。因此,JVM会把数组元素类型的类加载器记录为数组类型的类加载器。
2、双亲委托模式目的是什么?
目的:为了安全,而且各司其职,保证核心类库的安全性
当我们自己声明一个 java.lang.String 类,类加载器会为我们加载自定义的String类还是系统的String类呢?
当应用程序类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
当扩展类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
当引导类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会在它的负责的范围内尝试加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么会把这个任务往回传,让“子加载器”扩展类加载器去加载。
“子加载器”扩展类加载器接到“父加载器”返回的任务后,去它负责的范围内加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么会把这个任务往回传,让“子加载器”应用程序类加载器去加载。
“子加载器”应用程序类加载器接到“父加载器”返回的任务后,去它负责的范围内加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么就报错ClassNotFoundException或java.lang.NoClassDefError
四、获取 ClassLoader 对象
获取某个类的类加载对象需要两步:
① 获取某个类的 Class 对象;
② 通过 Class 对象调用 getClassLoader() 方法获取类加载器对象
五、java.lang.ClassLoader 对象
ClassLoader 类是一个抽象类,学习一下 ClassLoader 的相关方法:
public final ClassLoader getParent():返回委托的父类加载器。一些实现可能使用 null 来表示引导类加载器;
public static ClassLoader getSystemClassLoader():返回委托的系统类加载器;
public Class<?> loadClass(String name):使用指定的二进制名称(类的全限定名)来加载类。例如:java.lang.String,注意内部类的名称:匿名内部类(外部类的全限定名$编号)、局部内部类(外部类的全限定名$编号+类名)、成员/静态内部类(外部类的全限定名$+类名);
protected Class<?> findClass(String name):使用指定的二进制名称(类的全限定名)来查找类。此方法应该被类加载器的实现重写,该实现按照委托模型来加载类。在通过父类加载器检查所请求的类后,此方法将被 loadClass 方法调用;
protected final Class<?> findLoadedClass(String name):返回Class 对象,如果类没有被加载,则返回 null;
protected final Class<?> defineClass(String name,byte[] b,int off,int len):将一个 byte 数组转换为 Class 类的实例;
六、自定义类加载器
自定义加载器案例
1 import java.io.ByteArrayOutputStream;
2 import java.io.File;
3 import java.io.FileInputStream;
4 import java.io.FileNotFoundException;
5 import java.io.IOException;
6 import java.io.InputStream;
7
8 public class FileClassLoader extends ClassLoader{
9 private String rootDir;//指定加载路径
10
11 public FileClassLoader(String rootDir){
12 this.rootDir = rootDir;
13 }
14
15 @Override
16 protected Class<?> findClass(String name) throws ClassNotFoundException {
17 //首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经被装载,直接返回;
18 Class<?> c = findLoadedClass(name);
19
20 if(c ==null){
21 //委派类加载器请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;
22 ClassLoader parent = this.getParent();
23 try {
24 c = parent.loadClass(name);
25 //加异常处理,父类加载不到,然后自己加载
26 } catch (Exception e) {
27 }
28
29 //调用本类加载器的findClass()方法,试图获取对应的字节码,如果获取的到,则调用defineClass()导入类型到方法区;
30 //如果获取不到对应的字节码或其他原因失败,则异常,终止加载过程
31 if(c == null){
32 byte[] classData = getClassData(name);
33 if(classData == null){
34 throw new ClassNotFoundException();
35 }else{
36 c = defineClass(name, classData, 0, classData.length);
37 }
38 }
39 }
40 return c;
41 }
42
43 //把.class文件的内容读取到一个字节数组中
44 //为什么要读取的字节数组中,因为protected final Class<?> defineClass(String name,byte[] b,int off,int len)
45 private byte[] getClassData(String name) {
46 String path = rootDir + File.separator + name.replace(".", File.separator)+".class";
47 InputStream is = null;
48 ByteArrayOutputStream baos = null;
49 try {
50 is = new FileInputStream(path);
51 baos =new ByteArrayOutputStream();
52 byte[] buffer = new byte[1024];
53 int len;
54 while((len = is.read(buffer))!=-1){
55 baos.write(buffer, 0, len);
56 }
57 return baos.toByteArray();
58 } catch (FileNotFoundException e) {
59 e.printStackTrace();
60 } catch (IOException e) {
61 e.printStackTrace();
62 }finally{
63 try {
64 if(is!=null){
65 is.close();
66 }
67 } catch (IOException e) {
68 e.printStackTrace();
69 }
70 }
71 return null;
72 }
73 }
74
75 public class TestFileClassLoader {
76
77 public static void main(String[] args) throws ClassNotFoundException {
78 FileClassLoader fsc = new FileClassLoader("D:/java/code");
79 Class<?> uc = fsc.loadClass("com.ks.UserManager");
80 System.out.println(uc);
81
82 Class<?> sc = fsc.loadClass("java.lang.String");
83 System.out.println(sc);
84 System.out.println(sc.getClassLoader());//null,因为委托给父类加载器...一直到引导类加载器
85 }
86
87 }
七、获取不同的类加载器
1、案例一:获取类加载器
1 @Test
2 public void test1(){
3 //对于自定义类,使用系统类加载器进行加载
4 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
5 System.out.println(classLoader);
6
7 //调用系统类加载器的getParent():获取扩展类加载器
8 ClassLoader classLoader1 = classLoader.getParent();
9 System.out.println(classLoader1);
10
11 //调用扩展类加载器的getParent():无法获取引导类加载器
12 //引导类加载器主要负责加载java的核心类库,无法加载自定义的类。
13 ClassLoader classLoader2 = classLoader1.getParent();
14 System.out.println(classLoader2);
15
16 ClassLoader classLoader3 = String.class.getClassLoader();
17 System.out.println(classLoader3);
18
19 }
运行结果:
引导类加载器是无法获取的,返回为 null。
2、案例二
1 @Test
2 public void test2() throws ClassNotFoundException {
3 //1.获取一个系统类加载器
4 ClassLoader classloader = ClassLoader.getSystemClassLoader();
5 System.out.println(classloader);
6
7 //2.获取系统类加载器的父类加载器,即扩展类加载器
8 classloader = classloader.getParent();
9 System.out.println(classloader);
10
11 //3.获取扩展类加载器的父类加载器,即引导类加载器
12 classloader = classloader.getParent();
13 System.out.println(classloader);
14
15 //4.测试当前类由哪个类加载器进行加载
16 classloader = Class.forName("com.njf.java.ClassLoaderTest").getClassLoader();
17 System.out.println(classloader);
18
19 //5.测试JDK提供的Object类由哪个类加载器加载
20 classloader = Class.forName("java.lang.Object").getClassLoader();
21 System.out.println(classloader);
22 }
运行结果:
七、类加载器加载资源文件
ClassLoader 类的职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等。
1、案例一:加载类路径(如:src 下)jdbc.properties 资源文件
扩展:SourceFolder:源代码文件夹,一般会单独用一个config这种SourceFolder来装配置文件、等价于src,不同于普通的 Folder
代码:
在 src 下面创建 jdbc1.properties 文件,内容如下:
username=root
password=123456
url=jdbc:mysql://localhost:3306/test
读取配置文件值:
1 @Test
2 public void test2() throws Exception {
3
4 Properties pros = new Properties();
5 //此时的文件默认在当前的module下。
6 //读取配置文件的方式一:
7 // FileInputStream fis = new FileInputStream("jdbc.properties");
8 // FileInputStream fis = new FileInputStream("src\\jdbc1.properties");
9 // pros.load(fis);
10
11 //读取配置文件的方式二:使用ClassLoader
12 //配置文件默认识别为:当前module的src下
13 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
14 InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
15 pros.load(is);
16
17
18 String user = pros.getProperty("user");
19 String password = pros.getProperty("password");
20 System.out.println("user = " + user + ",password = " + password);
21
22
23
24 }
2、案例二:读取指定包下面文件
获取存放在“com.ks.reflect” 包下面的 demo.properties 文件
代码示例:
1 @Test
2 public void test() throws IOException{
3 Properties pro = new Properties();//集合,map,key=value
4
5 Class clazz = Test.class; //获取本类的 Class 对象
6 ClassLoader loader = clazz.getClassLoader(); //获取类加载器
7 InputStream in = loader.getResourceAsStream("com/ks/reflect/demo.properties");
8
9 pro.load(in);
10
11 System.out.println(pro);
12 System.out.println(pro.getProperty("name"));
13 }
3、单例三:读取其他目录下的文件
获取当前项目下的 out.properties 文件,不在 src 下面(这个文件没有在编译目录下,不需要使用类加载器)
代码实现:
1 @Test
2 public void test() throws IOException{
3 Properties pro = new Properties();
5 //在项目的根路径下,不在src里面
6 pro.load(new FileInputStream("out.properties"));
7
8 System.out.println(pro);
9 System.out.println(pro.getProperty("out"));
10 }