深入理解Java类加载器(一):Java类加载原理解析续

深入理解Java类加载器(一):Java类加载原理解析续_第1张图片

四、常见问题分析

1、由不同的类加载器加载的指定类还是相同的类型吗?
 在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中,一个类用其全名和 一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果,如下所示:

package com.code.selfclassload;

import java.io.IOException;
import java.io.InputStream;

public class TestBean
{
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException
    {
        //一个简单的类加载器,逆向双亲委派机制
        //可以加载与自己在同一路径下的Class文件
        ClassLoader myClassLoader = new ClassLoader()
        {
            @Override
            public Class loadClass(String name) throws ClassNotFoundException
            {
                String fileName = name.substring(name.lastIndexOf(".") +1) + ".class";
                InputStream is = getClass().getResourceAsStream(fileName);
                if (is == null)
                {
                    return super.loadClass(name);  //递归调用父类加载器
                }
                try
                {
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
                return super.loadClass(name);
            }
        };

        //通过自定义类加载器加载
        Object object = myClassLoader.loadClass("com.code.selfclassload.TestBean").newInstance();
        System.out.println(object.getClass());
        System.out.println(object.getClass().getClassLoader());
        //系统类加载器加载
        System.out.println(TestBean.class.getClassLoader());
        System.out.println(object instanceof com.code.selfclassload.TestBean);
    }
}

***输出:***
深入理解Java类加载器(一):Java类加载原理解析续_第2张图片
 我们发现,obj 确实是类classloader.test.bean.TestBean实例化出来的对象,但当这个对象与类classloader.test.bean.TestBean做所属类型检查时却返回了false。这是因为虚拟机中存在了两个TestBean类,一个是由系统类加载器加载的,另一个则是由我们自定义的类加载器加载的,虽然它们来自同一个Class文件,但依然是两个独立的类,因此做所属类型检查时返回false。
 
2、在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?
Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

分析一下对应的jdk的代码:

import sun.reflect.CallerSensitive;

@CallerSensitive
public static Class forName(String className)throws ClassNotFoundException {
        Class caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
        }

// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class caller) {
        // This can be null if the VM is requesting it
        if (caller == null) {
        return null;
        }
        // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader  
        return caller.getClassLoader0();
        }

3、在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?
  前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:

 protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
private static Void checkCreateClassLoader() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        return null;
    }

    @CallerSensitive
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

我们再来看一下对应的getSystemClassLoader()方法的实现:

private static synchronized void initSystemClassLoader() {  
    //...  
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
    scl = l.getClassLoader();  
    //...  
}  

我们可以写简单的测试代码来测试一下:

System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());

输出的结果为:

sun.misc.Launcher$AppClassLoader@18b4aac2

所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

  • /lib下的类;
  • /lib/ext下或者由系统变量java.ext.dir指定位置中的类;
  • 当前工程类路径下或者由系统变量java.class.path指定位置中的类。

4、在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到/lib下的类,但此时就不能够加载/lib/ext目录下的类了。
  Ps:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。
5、编写自定义类加载器时,一般有哪些注意点?

1)、一般尽量不要覆写已有的loadClass(…)方法中的委派逻辑(Old Generation)

一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  
public class WrongClassLoader extends ClassLoader {  

    public Class loadClass(String name) throws ClassNotFoundException {  
        return this.findClass(name);  
    }  

   protected Class findClass(String name) throws ClassNotFoundException {  
        // 假设此处只是到工程以外的特定目录D:\library下去加载类  
        // 具体实现代码省略  
    }  
}  

通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4中的结论就不成立了。大家可以简单测试一下,现在/lib、/lib/ext 和 工程类路径上的类都加载不上了。

//问题5测试代码一

public class WrongClassLoaderTest {  
    publicstaticvoid main(String[] args) {  
        try {  
            WrongClassLoader loader = new WrongClassLoader();  
            Class classLoaded = loader.loadClass("beans.Account");  
            System.out.println(classLoaded.getName());  
            System.out.println(classLoaded.getClassLoader());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}/* Output: 
        java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  
        at java.io.FileInputStream.open(Native Method)  
        at java.io.FileInputStream.(FileInputStream.java:106)  
        at WrongClassLoader.findClass(WrongClassLoader.java:40)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
        at java.lang.ClassLoader.defineClass1(Native Method)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
        at WrongClassLoader.findClass(WrongClassLoader.java:43)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
        at java.lang.ClassLoader.defineClass1(Native Method)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
        at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
        at WrongClassLoader.findClass(WrongClassLoader.java:43)  
        at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
        at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
 *///:~  

注意,这里D:”classes”beans”Account.class是物理存在的。这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。

//问题5测试二
//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)

public class WrongClassLoader extends ClassLoader {  
    protected Class findClass(String name) throws ClassNotFoundException {  
        //假设此处只是到工程以外的特定目录D:\library下去加载类  
        //具体实现代码省略  
    }  
}/* Output: 
        beans.Account  
        WrongClassLoader@1c78e57  
 *///:~ 

将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出正确。
2). 正确设置父类加载器
  通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。
3). 保证findClass(String name)方法的逻辑正确性
  事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。
6、如何在运行时判断系统类加载器能加载哪些路径下的类?
  一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty(“java.class.path”)。如下所示:

package com.code.selfclassload;
import com.google.gson.Gson;
public class Question6Test
{
    public static void main(String[] args)
    {
        System.out.println("Rico");
        Gson gson = new Gson();
        System.out.println(gson.getClass().getClassLoader());
        System.out.println(System.getProperty("java.class.path"));

}
}
深入理解Java类加载器(一):Java类加载原理解析续_第3张图片
 如上述程序所示,Test类和Gson类由系统类加载器加载,并且其加载路径就是用户类路径,包括当前类路径和引用的第三方类库的路径。
7、如何在运行时判断标准扩展类加载器能加载哪些路径下的类?
判断方法:

package com.code.selfclassload;

import java.net.URL;
import java.net.URLClassLoader;

public class ExtClassLoaderTest
{
    public static void main(String[] args)
    {
        try {
            URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();
            for (int i = 0; i < extURLs.length; i++) {
                System.out.println(extURLs[i]);
            }
        } catch (Exception e) {
            //…
        }
    }
}

file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_91/jre/lib/ext/zipfs.jar

五. 开发自己的类加载器

 在前面介绍类加载器的代理委派模型的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
  方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

 类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
  在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

package com.code.selfclassload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
// 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader
{
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    // 获取类的字节码
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);  // 获取类的字节数组
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        // 得到类文件的完全路径
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

 如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

 类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。加载本地文件系统上的类,示例如下:

package com.code.selfclassload;

public class Simple
{
    private Simple instance;

    public void setSample(Object instance) {
        System.out.println(instance.toString());
        this.instance = (Simple) instance;
    }
}

=================================================================================
package com.code.selfclassload;

import java.lang.reflect.Method;

public class ClassIdentity
{
    public static void main(String[] args) {
        new ClassIdentity().testClassIdentity();
    }

    public void testClassIdentity() {
        String classDataRootPath = "D:\\masterSpring\\jvm\\src\\main\\java\\com\\code\\selfclassload";
        FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
        FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
        String className = "com.code.selfclassload.Simple";
        try {
            Class class1 = fscl1.loadClass(className);  // 加载Sample类
            Object obj1 = class1.newInstance();  // 创建对象
            Class class2 = fscl2.loadClass(className);
            Object obj2 = class2.newInstance();
            Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
            setSampleMethod.invoke(obj1, obj2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:
com.code.selfclassload.Simple@6d6f6e28

你可能感兴趣的:(JVM)