专业的JAVA编程教程与资源

网站首页 > java教程 正文

深度分析ClassLoader机制,不可错过这一篇

temp10 2024-10-13 09:28:02 java教程 9 ℃ 0 评论

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

深度分析ClassLoader机制,不可错过这一篇


定义

ClassLoader类,它是一个抽象类,sun公司给出定义:


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


loadClass()方法


内容如下:

使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:

  • 调用findLoadedClass(String)方法检查这个类是否被加载过
  • 使用父加载器调用loadClass(String)方法,如果父加载器为Null,使用BootStrapClassLoader加载
  • 调用findClass(String)方法装载类


如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。


备注:

ClassLoader的子类最好覆盖findClass(String)而不是这个方法(loadClass)。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)


protected Class<?> loadClass(String name, boolean resolve) 该方法的访问控制符是protected,即该方法同包内和派生类中可用


返回值类型Class <?>,这里用到泛型。这里为什么使用通配符?

作为泛型实参表示对象可以接受任何类型(类类型)。因为该方法不知道要加载的类到底是什么类,所以就用了通用的泛型。

String name要查找的类的名字,boolean resolve,一个标志,true表示将调用resolveClass(c)处理该类

throws ClassNotFoundException 该方法会抛出找不到该类的异常,这是一个非运行时异常


getClassLoadingLock()方法


此处使用变量parallelLockMap,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock)方法来给刚刚创建好的对象赋值。


那么这个parallelLockMap变量又是哪来?

源码中发现这个变量是ClassLoader类的成员变量:

private final ConcurrentHashMap<String, Object> parallelLockMap;


在ClassLoader类中有一个静态内部类ParallelLoaders,它会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么它就给parallelLockMap定义,就是new一个 ConcurrentHashMap<>()。

如果当前的加载器是具有并行能力的,那么parallelLockMap就不是Null,上图中通过判断parallelLockMap是不是Null,如果是null,说明该加载器没有注册并行能力,那么就没有必要给它一个加锁的对象,getClassLoadingLock方法直接返回this,就是当前的加载器的一个实例。

如果这个parallelLockMap不是null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMap的putIfAbsent(className, newLock)方法。


扩展:

putIfAbsent()方法的作用:

首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把它关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。

问题 getClassLoadingLock(name)方法的作用是什么?


为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。


findLoadedClass()方法


Class c = findLoadedClass(name),在这里,在加载类之前先调用该方法检查该类是否已经被加载过,findLoadedClass会返回一个Class类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve的值来决定是否处理该对象)。如果该类没有被加载过,那么执行以下的加载过程。

如果父加载器不为空,那么调用父加载器的loadClass方法加载类,如果父加载器为空,那么调用BootStrapClassLoader来加载类。


如果以上两个步骤都没有成功的加载到类。那么执行以下过程:

c = findClass(name)表示当前classloader自己来加载类。


执行上图后,如果已经得到了加载之后的类,那么就根据resolve的值决定是否调用resolveClass方法;如果没有加载到,则抛出ClassNotFoundException 异常。

resolveClass方法的作用:

链接指定的类这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java?规范中的Execution描述进行链接。



扩展:


类装载方式:

  • 隐式装载

程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。

  • 显式装载,

通过class.forname()等方法,显式加载需要的类。


类加载的动态性体现

一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现。


java类装载器

Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下:

为什么要有三个类加载器?

一方面是分工,各自负责各自的区块,另一方面为了实现委托模型。


类加载器之间是如何协调工作?

java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢?

Java采用了委托模型机制来加载类,就是类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类,如果搜索不到,则抛出ClassNotFoundException


类加载工作由ClassLoder和其子类负责。JVM在运行时会产生三个ClassLoader:BootStrapClassLoader,ExtClassLoader和AppClassLoader,其中:

BootStrapClassLoader不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,如java.*,。

ExtClassLoader是ClassLoder的子类,负责装载JRE扩展目录ext下的jar类包;

AppClassLoader负责装载classpath路径下的类包。


关系:

这三个类装载器存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下使用AppClassLoader装载应用程序的类


eg:


运行结果:


可以看出Test是由AppClassLoader加载器加载的,AppClassLoader的Parent 加载器是ExtClassLoader,但是ExtClassLoader的Parent为 null 是为什么呢?

前面有提到BootStrapLoader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,就会看到输出为null。


Java加载类使用全盘负责委托机制。

全盘负责是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入;

委托机制是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。


双亲委派模式类加载好处

  • 避免类的重复加载,同一个类只加载一次

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

  • 安全因素

java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。


可能会想,如果在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang


类文件被装载解析后,在JVM中都有一个对应的java.lang.Class对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void都拥有对应的Class对象。Class类没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表