您的位置:首页 > 其它

ClassLoader扒开了给你看

2016-07-19 15:19 183 查看


ClassLoader

本节回答什么是ClassLoader,为啥需要ClassLoader,有了这两个问题的答案之后,我们回答一个类的Classloader是谁?首先什么是ClassLoader:
介绍ClassLoader之前必须要介绍一个Class,用过反射的人都知道这个Class对象是整个反射的基础,没有他就没有很多其他的乱起八糟的反射的东西了,Class对象由JVM维护。
ClassLoader是类加载器,提供给JVM将一个.class格式的字节流转换为一个Class对象,JVM依靠Class对象这种元信息来管理所有的类,只有JVM加载了这种Class对象,JVM才可以使用对应的类,否则JVM就会抛ClassNotFoundException的异常。
为什么需要ClassLoader,有了ClassLoader可以动态的将一个.class加载到JVM中,还可以自己控制类的加载过程。

那么一个类的ClassLoader是谁呢?当然是哪个ClassLoader加载了这个类那么这个类的ClassLoader就是谁了,问题的关键其实是一个类会由什么ClassLoader加载以及什么时候加载这个类。我们看一下ClassLoader的源代码:
public abstract class ClassLoader {
//指向parent的指针
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)   throws ClassNotFoundException{
// 首先检查是否已经被当前这个加载器加载了,由JVM来check
Class c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
//先交给parent去load
c = parent.loadClass(name, false);
} else {
//还没有的话跟bootstrap要
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//主要抛出ClassNotFoundException
c = findClass(name);
}
//链接
resolveClass(c);
return c;
}


观察一下这个类,首先它不是final的,所以可以继承,也就是说我们可以自定义一个Classloader,再观察我们发现他的关键方法:loadClass(String name)不是final的也不是private的,所以我们同样可以override,也就是说我们可以重新定义类的加载过程。另外我们发现当加载一个类时首先会去看看当前这个ClassLoader有没有加载过这个类,注意是当前这个ClassCloader,也就是说如果其他ClassLoader加载了这个类,JVM用了另一个ClassLoader又加载这个类时会告诉你没有找到的;结论:
一个类是否是唯一的其实分编译时跟运行时,编译时是靠域名确定的,但是运行时实际上_ClassLoader+域名_才能唯一确定一个类
一个类的加载过程是可以自定义的,你可以很无耻的在每加载一个类的时候都print你的花名。
眼尖的人会发现上面有个问题的答案还没有给出,后面继续道来。


JVM的类加载原理

JVM自己定义了几个初始的ClassLoader,Bootstrap,Ext以及App。区别以及联系如下:

区别:
Bootstrap负责加载JVM核心库的jar包跟类,比如说典型的rt.jar,这里面有java的容器以及并发包等
Ext负责加载$JAVA_HOME/jre/lib/ext/目录下的jar包和类
App负责加载CLASSPATH下的所有的jar包和类

联系:
他们是父子关系,Bootstrap是Ext的parent,Ext是App的parent,看图: 



双亲委派机制

从默认的ClassLoader的源代码中不难看出,jvm ClassLoader类的loadClass的工作过程为:
检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2
如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
请求parent classloader载入,如果成功到8,不成功到5
请求jvm从bootstrap classloader中载入,如果成功到8
寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
从文件中载入Class,到8.
抛出ClassNotFoundException.
返回Class.

ClassLoader的双亲委派机制很多人都了解,我们从另一个角度来讲,顺便回答一下上面遗漏的问题。双亲委派机制的含义就是先看双亲有没有,双亲没有了再自己加载,这是个迭代的过程。为了防止一个类被多次加载导致资源浪费等等的问题,其必须保证同样一个类被加载一次。反过来如果不采用双亲委派的方式加载类的话,那就可能Ext加载了某个类,然后App又加载了这个类。这样就导致上图中有两份Class的实例,两份不光是资源的浪费,也会让开发者面临很多问题,比如需要自己选择自己需要的Class。但是也带来了另一个问题:jar包冲突。如下图所示:





Application依赖notify跟hadoop,notify依赖protobuf2.3跟2.5两个版本,糟糕的是protobuf的两个版本不兼容,那么双亲委派机制就带来灾难了,因为如果加载了某个版本的类,依赖另一个版本的所有功能就无法使用,报的肯定是跟Class元信息有关的异常。

另外再解一下上面的问题,如果某一个在使用的时候发现还没有加载,那么会由什么ClassLoader来加载呢?我们当然可以自己制定一个ClassLoader并用它来load这个class,大部分情况下是由JVM来负责制定一个ClassLoader来加载的,那这个ClassLoader是哪个呢?答案是this.getClass().getClassLoader(),入口一般都是一个线程,所以大部分的类都是有Thread.currentThread.getContextClassLoader()来加载的,也就是加载这个类的线程的ClassLoader来加载,如果一个类A发现另一个类B没有,那么会调用类A的ClassLoader来加载类B;总结:
java默认的加载原则为双亲委派原则
双亲委派原则会导致jar包冲突的问题,解决方案请看后面
一个类的class是由this.getClass().getClassLoader()加载的
问你一个问题:我们可以自己写一个java.lang.system的类么?如果能,为什么?如果不能,为什么?


JVM的线程类加载器

线程上下文类加载器是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源,为什么需要这个ContextClassLoader呢?因为双亲委派的方式并不能解决
Java 应用开发中会遇到的类加载器的全部问题!Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现,比如说JDBC,JAXP 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。作者本身就遇到过跟JAXP相关的蛋疼的问题。这些 SPI 的impl代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,JAXP的最常见的实现是apache的xerces。SPI
接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,这是由SPI 的impl来提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI
的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。


常见的ClassLoader实现


osgi的ClassLoader(felix)

介绍一下osgi(open service gateway initiative这是一个倡议),一句话来介绍osgi能干嘛:osgi能使每一个jar(bundle)包有一个自己的ClassLoader。很多人看到这里似乎对于在[双亲委派机制]一节中提出的一个开放的问题有了新的答案了。osgi典型的满足层次结构的,如下所示;另外osgi本身不提供实现,实现了osgi的有apache的felix还有Eclipse的Equinox。osgi的东西有很多,我们这里只讲osgi中的ClassLoader。我们以felix为例来讲osgi的ClassLoader,讲之前有必要说一下osgi的bundle。
执行环境(Execution Environment)。由于OSGi所适用的目标范围非常广泛,为了更好地处理不同硬件、软件环境对OSGi造成的兼容性问题,在建立其他约定之前,必须先定义好系统的执行环境。
安全层(Security Layer)。描述了基于Java 2安全架构实现的代码验证、JAR文件数字签名、数字证书服务,安全层贯穿了OSGi框架的其他各个层次。
模块层(Module Layer)。模块层从“静态”的角度描述了一个模块的元数据信息、执行环境定义、模块约束和解析过程、类加载顺序等内容。模块层是整个OSGi中最基础、最底层的层次。
生命周期层(Life Cycle Layer)。生命周期层从“动态”的角度描述了一个模块从安装到被解析、启动、停止、更新、卸载的过程,以及在这些过程中的事件监听和上下文支持环境。
服务层(Service Layer)。描述了如何定义、注册、导出、查找、监听和使用OSGi中的服务。服务层是所有OSGi标准服务的基础。
框架API(Framework API)。由一系列通过Java语言实现的接口和常量类构成,为上面各层提供面向Java语言的编程接口。

bundle即是上面的模块层,更简单一点,bundle是osgi规范的jar包,里面必须定义你可以导出和需要引入的模块以及其版本。osgi为每一个bundle都定义了一个ClassLoader,每一个bundle的依赖关系,也就是ClassLoader之间类的依赖关系由osgi来维护。举个简单的类加载的例子,你在bundleA中引入了bundleB的类那么会由bundleB的ClassLoader向bundleA提供这个Class。我们来看一下OSGI的类加载源码。


URLClassLoader


tomcat的ClassLoader

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: