JVM自带的类加载器:

其关系如下:

其中,类加载器在加载类的时候是使用了所谓的“父委托”机制。其中,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器。
关于父委托机制的说明:

当生成 一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器将成为该类加载器的父类加载器
下面,自定义类加载器。自定义的类加载器必须继承java.lang.ClassLoader类
- import java.io.*;
- public class MyClassLoader extends ClassLoader {
- private String name; //类加载器的名字
- private String path; //加载类的路径
- private final String fileType = ".class"; //class文件的扩展名
- public MyClassLoader(String name){
- super(); //让系统类加载器成为该类加载器的父 类加载器,该句可省略不写
- this.name = name;
- }
- public MyClassLoader(ClassLoader parent,String name){
- super(parent); //显示指定该类加载器的父 类加载器
- this.name = name;
- }
- @Override
- public String toString() {
- return this.name;
- }
- public String getPath() {
- return path;
- }
- public void setPath(String path) {
- this.path = path;
- }
- //实现自定义的类加载器必须重写findClass方法,否则ClassLoader类中的findClass()方法是抛出了异常
- @Override
- public Class findClass(String name)throws ClassNotFoundException{
- byte[] data = this.loadClassData(name);
- return this.defineClass(name,data,0,data.length);
- }
- private byte[] loadClassData(String name){
- InputStream is = null;
- byte[] data = null;
- ByteArrayOutputStream baos = null;
- try {
- this.name = this.name.replace(".","\\"); //com.dream.it---->com\dream\it
- is = new FileInputStream(new File(path + name + fileType));
- int ch;
- while(-1 != (ch = is.read())){
- baos.write(ch); //将数据写入到字节数组输出流对象中去
- }
- data = baos.toByteArray();
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- try {
- is.close();
- baos.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return data;
- }
- public static void main(String[] args) throws Exception {
- MyClassLoader loader1 = new MyClassLoader("loader1");
- loader1.setPath("d:/myapp/serverlib/");
- MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); //loader1作为loader2的父 类加载器
- loader2.setPath("d:/myapp/clientlib");
- MyClassLoader loader3 = new MyClassLoader(null,"loader3");//父类加载器为null,表明其父类加载器为根类加载器
- loader3.setPath("d:/myapp/otherlib");
- test(loader2);
- test(loader3);
- }
- public static void test(ClassLoader cl) throws Exception {
- Class clazz = cl.loadClass("Sample");
- Object object = clazz.newInstance();
- }
- }
附上findClass()方法的JDK说明
- protected Class<?> findClass(String name) throws ClassNotFoundException
- Finds the class with the specified binary name.
- This method should be overridden by class loader
- implementations that follow the delegation model
- for loading classes, and will be invoked by the
- loadClass method after checking the parent class
- loader for the requested class. The default
- implementation throws a ClassNotFoundException.
大致说明一下意思:通过指定的name来查找类。该方法应该被类加载器的实现类重写,从而能够保证在加载类的时候可以遵循委托机制模型。在loadClass()方法(该方法是由JVM调用的)中,检查其父类加载器之后,该方法再被调用去加载请求的类。默认该方法的实现是抛出了一个ClassNotFoundException异常。
其实,所谓的加载类,无非就是读取.class文件到内存中,所以在findClass()方法中,loadClassData()方法用于读取.class文件的数据,并返回一个字节数组。然后利用ClassLoader类的defineClass()方法将字节数组转换为Class对象。
上述自定义的类加载器loader1,loader2,loader3及JVM自带的类加载器之间的关系如下:

对于各个类加载器,系统的类加载器是从环境变量classpath中读取.class文件实现类的加载;loader1是从目录d:/myapp/serverlib/下读取.class文件;loader2是从目录d:/myapp/clientlib/下读取.class文件,loader3是从目录d:/myapp/otherlib/下读取.class文件
执行结果:

此处我们分析一下出现这种执行结果的原因:
当执行loader2.loadClass(“Sample”)时先由它上层的所有父类加载器尝试加载Sample类。
loader1从D:\myapp\serverliv目录下成功加载了Sample类,所以loader1是Sample类的定义类加载器,loader1和loader2是Sample类的初始类加载器。
当执行loader3.loadClass(“Sample”)时,先由它上层的所有父类加载器尝试加载Sample类。
loader3的父加载器为根类加载器,它无法加载Sample类,接着loader3从D:\myapp\otherlib目录下成功加载Sample类,所以loader3是Sample类的定义类加载器及初始类加载器。
在Sample类中主动使用了Dog类(new Dog()),当执行Sample类的构造方法中的new Dog()语句时,JVM需要先加载Dog类,到底用哪个类加载器家在呢?
从上述的打印结果中可以看出,加载Sample类的loader1还加载了Dog类,JVM会用Sample类的定义类加载器去加载Dog类,加载过程中也同样采用了父亲委托机制。
为了验证这一点,可以吧D:\myapp\serverlib目录下Dog.class文件删除,然后在D:\myapp\syslib目录下存放一个Dog.class文件,此时打印结果如下:
- Sample:loader1
- Dog:sun.misc.Launcher$AppClassLoader@1b84c92
- Sample:loader3
- Dog:loader3
由此可见,当由loader1加载的Sample类首次主动使用Dog类时,Dog类由系统类加载器加载,如果把D:\myapp\serverlib和D:\myapp\syslib目录下的Dog.class文件都删除,然后在D:\myapp\client目录下存放一个Dog.class文件。
此时文件结构如下图所示:

当Loader1加载Sample类首次主动使用Dog类时,由于loader1及其父类加载器都无法加载Dog类,因此test(loader2)会抛出ClassNotFoundExcption.
这又是因为什么原因呢?
这又牵扯到命名空间的问题。
同一个命名空间内的类时相互可见的。
子加载器的命名空间包含所有父类加载器的命名空间,因此由子加载器加载的类能看见父类加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
对于上述问题,loader1可以加载Sample类,而Dog类只能由loader2加载Dog类,loader1是Loader2的父类加载器,父加载器loader1加载的类Sample不能看见子加载器loader2加载的类Dog,所以会抛出异常。
对于上述实例中的main方法,我们不调用test方法,换成如下代码
- Class clazz = loader1.loadClass("Sample");
- Object obj = clazz.newInstance();
- Sample sample = (Sample)obj;
- System.out.println(sample.v1);
MyClassLoader类由系统类加载器加载,而Sample类由loader1类加载器加载,所以MyClassLoader类看不见Sample类。在MyClassLoader类的main方法中使用Sample类,会导致NoClassFoundError错误。
当两个不同命名空间内的类相互不可见时,可采用Java反射机制来访问对象实例的属性和方法。
将上述代码修改:
- Class clazz = loader1.loadClass("Sample");
- Object obj = clazz.newInstance();
- Field field = clazz.getField("v1");
- int v1 = field.getInt(obj);
- System.out.println(v1);
此时,可以获取到对象中的v1属性值。利用反射机制,我们可以跨越这种命名空间的限制。
补充:
命名空间:

运行时包:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持w3xue。如有错误或未考虑完全的地方,望不吝赐教。