Java 类加载器 - TongtongLan/Java GitHub Wiki
JVM预定义的三种类加载器,当JVM启动的时候,Java缺省开始使用三种类型的类加载器,此外还可使用用户自定义类加载器
Bootstrap是本地代码编写的(例如C), ExtClassLoader、 AppClassLoader是java代码,都在rt.jar中,且都是sun.misc.Launcher内部类,并由Bootstrap类加载器加载
启动(Bootstrap)类加载器:引导类加载器是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中(可以通过 System.getProperty("sun.boot.class.path") 查看加载路径)。由于类加载器是使用平台相关的底层C/C++语言实现的,开发者无法直接获取到启动类加载器的引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,可以直接使用null代替(详见 java.lang.ClassLoader)。但是,我们可以查询某个类是否被引导类加载器加载过。
一般而言,{JRE_HOME}/lib下存放着JVM正常工作所需要的系统类,如下表所示:(可以查看JRE System Library中 <JAVA_HOME>/lib下的内容)
-
rt.jar:运行环境包,rt即runtime,J2SE 的类定义都在这个包内
-
charsets.jar:字符集支持包
-
jce.jar:是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现
-
jsse.jar:安全套接字拓展包Java(TM) Secure Socket Extension
-
classlist:该文件内表示是引导类加载器应该加载的类的清单
-
net.properties:JVM 网络配置信息
启动类加载器加载系统类之后,JVM结构如下:
系统类被加载到方法区,维持一个对类的实力引用。
-
引导类加载器将类信息加载到方法区中,以特定方式组织,对于某一个特定的类而言,在方法区中它应该有 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用,对应class实例的引用等信息。
-
类加载器的引用,由于这些类是由引导类加载器(Bootstrap Classloader)进行加载的,而 引导类加载器是有C++语言实现的,所以是无法被直接引用,故而该引用为NULL。用户在编写自定义类加载器时,如果需要把加载请求委托给引导类加载器,直接使用null代替即可(可查看 java.lang.Classloader.getClassLoader() 方法)
-
对应class实例的引用, 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。
扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。拓展类加载器是是整个JVM加载器的Java代码可以访问到的类加载器的最顶端,即是超级父加载器,拓展类加载器是没有父类加载器的。开发者可以直接使用标准扩展类加载器。
具体加载路径可通过
System.getProperty("java.ext.dirs")查看。在使用Java运行程序时,可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld。
应用程序类加载器(Applocatoin Class Loader):系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径。应用类加载器将拓展类加载器当成自己的父类加载器,当其尝试加载类的时候,首先尝试让其父加载器-拓展类加载器加载;如果拓展类加载器加载成功,则直接返回加载结果Class instance;加载失败,则会询问是否引导类加载器已经加载了该类;只有没有加载的时候,应用类加载器才会尝试自己加载。由于xxx.x.xxx.x.x.XClass是整个用户代码的入口,在Java虚拟机规范中,称其为初始类(Initial Class).开发者可以直接使用系统类加载器。
如果应用程序没有自定义类加载器,一般情况下应用程序类加载器(Applocatoin Class Loader)即为默认加载器。
用户自定义类加载器(Customized Class Loader):用户可以自己定义类加载器来加载类。所有的类加载器都要继承java.lang.ClassLoader类。
底层实现机制可见 java.lang.ClassLoader详解
对于某个特定的类加载器而言,应该为其指定一个父类加载器(模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器),当用其进行加载类的时候:
-
委托父类加载器帮忙加载;
-
父类加载器加载不了,则查询引导类加载器有没有加载过该类(这里有歧义,到底是委托启动类加载器加载还是?重点在于扩展类加载器是否把启动类加载器作为对待对待);
-
如果引导类加载器没有加载过该类,则当前的类加载器应该自己加载该类;
-
若加载成功,返回 对应的Class 对象;若失败,抛出异常“ClassNotFoundException”。
双亲委派模型中的"双亲"并不是指它有两个父类加载器的意思,一个类加载器只应该有一个父加载器。
-
父类加载器(parent classloader):它可以替子加载器尝试加载类
-
引导类加载器(bootstrap classloader): 子类加载器只能判断某个类是否被引导类加载器加载过,而不能委托它加载某个类;换句话说,就是子类加载器不能接触到引导类加载器,引导类加载器对其他类加载器而言是透明的。
Java 类随着他的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object ,无论任何类要加载这个类,最终都会委派给处于模型最顶端的启动类加载器,以此来保证 Object 类在程序中都是同一个类(类的唯一性由它本身及其类加载器确定),并且防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。
JVM 方法区的类信息区是按照类加载器进行划分的,每个类加载器会维护自己加载类信息(即命名空间,详见下方解释);
某个类加载器在加载相应的类时,会相应地在JVM内存堆(Heap)中创建一个对应的Class,用来表示访问该类信息的入口。
线程上下文类加载器是从线程的角度来看待类的加载,为每一个线程绑定一个类加载器,可以将类的加载从单纯的 双亲加载模型解放出来,进而实现特定的加载需求。
这个类加载器可以通过 java.lang.Thread 类的 setContentClassLoaser() 方法进行设置,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
JNDI 服务使用这个线程上下文加载器加载所需的 SPI 代码,也就是父类加载器请求子类加载器完成类加载的动作。Java 中涉及到 SPI 的加载动作基本上都是采用这种方式,例如JNDI 、JDBC、JCE、JAXB、JBI等。
每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。
命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护。
jvm为每个类加载器维护的一个“表”,这个表记录了所有以此类加载器为“初始类加载器”(而不是定义类加载器,所以一个类可以存在于很多的命名空间中)加载的类的列表,所以: CLTest是AppClassloader加载的,String是通过加载CLTest的类加载器也就是AppClassloader进行加载,但最终委派到bootstrap加载的(当然,String类其实早已经被加载过了,这里只是举个例子)。所以,对于String类来说,bootstrap是“定义类加载器”,AppClassloader是“初始类加载器”。根据刚才所说,String类在AppClassloader的命名空间中(同时也在bootstrap,ExtClassloader的命名空间中,因为bootstrap,ExtClassloader也是String的初始类加载器),所以CLTest可以随便访问String类。这样就可以解释“处在不同命名空间的类,不能直接互相访问”这句话了。
可以考虑一下两个自定义类加载器分别加载的两个类的命名空间问题。
如果用“自定义clsloadr1”加载java.lang.String类,那么根据双亲委派最终bootstrap会加载此类,那么bootstrap类就叫做该类的“定义类加载器”,而包括bootstrap的所有得到该类class实例的类加载器都叫做“初始类加载器”。