从jdk源码角度理解jvm类加载机制

来源:互联网 发布:手机直播系统源码 编辑:程序博客网 时间:2024/05/29 03:13

        关于jvm类加载机制,个人感觉还是挺有深度的,可能一般写代码关注业务居多,对jvm的一些机制关注太少,只知其表,而不然其因,实在肤浅。这样写代码估计也写不出优雅的代码来。

         网络上关于jvm类加载机制的文章实在是太多,但是从jdk源码角度来理解的确实比较少,之前也看到一篇优秀的博客:深入浅出ClassLoader,非常有深度地讲解了类加载机制。这里关注的是从jdk源码角度来理解。

一.  委派机制(delegation model)

        如果你看过sun.misc.Launcher、java.lang.ClassLoader源码的话,可能对”委派机制“并不陌生,下面来讲讲jdk是如何去做的。

先看一张jvm类加载器类的关系图,这是jdk源码体现关系图



        从这张图,可以看出,所有的ClassLoader都是继承于java.lang.ClassLoader来实现的,当然jvm 中底层C++实现的Bootstrap ClassLoader除外,这个类加载器,等下再说。

       再来看看另一张图,这是类加载委派关系图


这里说的”委派“在jdk源码中主要是这样体现的:

       1)在java.lang.ClassLoader中有一个属性”parent“,其解释如下

   // The parent class loader for delegation    // Note: VM hardcoded the offset of this field, thus all new fields    // must be added *after* it.    private final ClassLoader parent;

       2)在java.lang.ClassLoader中有一个protected 方法loadClass,如下:

protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException    {        synchronized (getClassLoadingLock(name)) {            // First, check if the class has already been loaded            Class c = findLoadedClass(name);            if (c == null) {                long t0 = System.nanoTime();                try {                    if (parent != null) {                        c = parent.loadClass(name, false);                    } else {                        c = findBootstrapClassOrNull(name);                    }                } catch (ClassNotFoundException e) {                    // ClassNotFoundException thrown if class not found                    // from the non-null parent class loader                }                if (c == null) {                    // If still not found, then invoke findClass in order                    // to find the class.                    long t1 = System.nanoTime();                    c = findClass(name);                    // this is the defining class loader; record the stats                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }
        ClassLoader loadClass函数是用来加载class的,当前ClassLoader去加载class时,首先判断其“parent”属性的类加载器,如果不为null,则首先让“parent”类加载器去加载,这样按照“类加载委派关系图”一层层往上推;如果,其委派层次上面的“parent”类加载器加载失败,最后由当前的类加载器去加载。这里需要注意的是,虽然OtherClassLoader的“parent”属性指向AppClassLoder,AppClassLoder的“parent”属性指向ExtClassLoder,但是ExtClassLoder的“parent”属性并不是指向Bootstrap ClassLoder,而是为null,当然Bootstrap ClassLoder的“parent”也为null。请看源码:

1)ExtClassLoader的构造函数,第二个参数为null,即赋值给“parent”的值为null:

/*         * Creates a new ExtClassLoader for the specified directories.         */        public ExtClassLoader(File[] dirs) throws IOException {            super(getExtURLs(dirs), null, factory);        }
2)AppClassLoader的构造函数,第二个参数为extcl,这个参数实际上指的是ExtClassLoader,会赋值为“parent”属性:
 // Now create the class loader to use to launch the application        try {            loader = AppClassLoader.getAppClassLoader(extcl);        } catch (IOException e) {            throw new InternalError(                "Could not create application class loader");        }
        当然,很多人会有疑问,Bootstrap ClassLoder、ExtClassLoader、AppClassLoader这么多ClassLoader,它们是从哪里加载class的,这个问题jdk源码中sun.misc.Launcher已经给出回答:Bootstrap ClassLoder加载的是System.getProperty("sun.boot.class.path");、ExtClassLoader加载的是System.getProperty("java.ext.dirs")、AppClassLoader加载的是System.getProperty("java.class.path"),以最简单的java工程,一个main方法,一条简单语句,运行环境为例说明这些路径下到底有哪些jar:

1)sun.boot.class.path = C:\Program Files (x86)\Java\jre7\lib\resources.jar;C:\Program Files (x86)\Java\jre7\lib\rt.jar;C:\Program Files (x86)\Java\jre7\lib\jsse.jar;C:\Program Files (x86)\Java\jre7\lib\jce.jar;C:\Program Files (x86)\Java\jre7\lib\charsets.jar;C:\Program Files (x86)\Java\jre7\lib\jfr.jar

看到了把,都是jre lib(注意这里说的jre是java路径下的,不是jdk路径下的jre,下同)下面的jar,都是java中最基本的jar,例如rt.jar、resources.jar等;

2)java.ext.dirs = C:\Program Files (x86)\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext,lib下面的ext路径;

3)java.class.path = E:\java_web\Test\bin;当前工程编译后的bin路径

        这样,相信委派机制应该说的很清楚了。

二.  如何实现自己的ClassLoader

        这个问题其实比较深奥,为什么这么说,因为类加载在一个java系统中占有非常重要的地位,它是class进入jvm的一个入口,如果入口都有问题,那这个系统应该没有什么意义。业界比较有名的类加载机制有:委派机制的典型代表“tomcat类加载机制”、颠覆委派机制的“osgi类加载”,有兴趣的话,可以自行研究,这里只说说简单的用法。

       在java.lang.ClassLoader的loadClass注释中有这么一段话:* Subclasses of ClassLoader are encouraged to override #findClass(String), rather than this method.因为loadClass函数中调用了findClass函数,loadClass函数已经实现了“委派机制”,你只要去实现findClass就可以了,所以jdk是建议实现findClass就可以了,注意,这只是一个建议而已,当然如果你不想要jdk的“委派机制”,也可以自行写loadClass,所以这就为osgi的类加载留下了发展的空间。至于说到底“委派机制”、osgi类加载,哪个更优,只能说各有各的优缺点,只有你的项目需求才能给出答案,这里不做深入讨论。只是谈谈“委派机制”的一般常用用法:

1)实现findClass的类加载:

/** * 实现“委派机制”中的findClass * @param name the binary name of the class, eg.org.test.ClassLoaderTest * */@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {byte[] b = null;try {b = loadLocalClass(name);} catch (URISyntaxException e) {e.printStackTrace();}if(b!=null) {// 将class bin转为Class objectreturn defineClass(name, b, 0, b.length);}return null;}/** * 读取class bin文件,这里是以读取E:\下的class为例 * */private byte[] loadLocalClass(String name) throws URISyntaxException {DataInputStream dis = null;try {int index = name.lastIndexOf(".");String className = name.substring(index+1);String path = "E:\\"+className+".class";File file = new File(path).getCanonicalFile();dis = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));byte[] tmpArr = new byte[1024];int readLen = 0;readLen = dis.read(tmpArr);byte[] byteArr = new byte[0];while(readLen>0) {byteArr = mergeArray(byteArr,tmpArr,readLen);readLen = dis.read(tmpArr);}return byteArr;} catch (SecurityException se) {se.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {// 关闭流if(dis!=null) {try {dis.close();} catch (IOException e) {e.printStackTrace();}}}return null;}private byte[] mergeArray(byte[] byteArr1, byte[] byteArr2, int len) {int size = byteArr1.length+len;byte[] byteArr = new byte[size];System.arraycopy(byteArr1, 0, byteArr, 0, byteArr1.length);System.arraycopy(byteArr2, 0, byteArr, byteArr1.length, len);return byteArr;}

2)用java.net.URLClassLoader实现

       在“jdk源码体现关系图”中也看到了,ExtClassLoader、AppClassLoader都是继承于URLClassLoader的,所以可以直接用URLClassLoder指明URL就可以了,如下:

URLClassLoader loaderTest = null;try {loaderTest = new URLClassLoader(new URL[]{new File("E:\\process.jar").toURI().toURL()});} catch (MalformedURLException e3) {// TODO Auto-generated catch blocke3.printStackTrace();}try {// 测试加载E:\下的Process1.classClass<?> pro = loaderTest.loadClass("org.test.Process1");} catch (ClassNotFoundException e) {e.printStackTrace();}
需要注意的是:URLClassLoader支持两种file形式的资源,一种是jar文件,一种是directory,以process.jar为例说明:

在process.jar中有一个类是org.test.Process1,简单写得,其中org.test是包名;那传给File的参数就可以直接是"E:\\process.jar";另一种方式传递路径,即给File的参数是"E:\\",这里就需要自行在E:\\下放org文件夹,org里面在放test文件夹,test里面在放Process1.class文件,其实就是copy编译后的带包名的路径。

三.  模拟“委派类加载机制”的行为

1)验证“委派机制”委派行为

       这里主要是写一个类,在构造函数中,输出不同的结果,放在不同的路径下,例如,放在jre\lib\ext下(ExtClassLoader来加载),当然java工程本地的bin路径下会有编译后的.class文件,如下:

package org.test;public class Process1 {public Process1() {System.out.println("ExtClassLoad load Process1.class");}public static void main(String[] args) {Process1 pro = new Process1();}}

将待输出“ExtClassLoad load Process1.class”结果的Process1.java导出jar包, 放在jre\lib\ext路径下;再修改Process1.java输出结果,改为System.out.println("AppClassLoad load Process1.class");,编译java工程,这样在本地工程的bin路径下会有输出"AppClassLoad load Process1.class"的Process1.class,运行Process1.java,Console会输出“ExtClassLoad load Process1.class”而不是"AppClassLoad load Process1.class",因为ExtClassLoader会先加载Process1.class,把jre\lib\ext\路径下关于Process1的jar删掉,在运行Process1.java,控制台就会输出"AppClassLoad load Process1.class",这个时候就是AppClassLoader来加载。

当然,ExtClassLoader在加载jre\lib\ext时,也支持directory方式,与URLClassLoader不同的是,需要先ext下新建一层文件夹,然后在这个文件夹下放置带包名的.class文件。

2)模拟类加载异常:ClassNotFoundException、NoSuchMethodException、ClassCastException、NoClassDefFoundError

        以下模拟是接着上面的类及包名。

增加一个类ClassLoaderTest:

public class ClassloaderTest extends URLClassLoader {public ClassloaderTest(URL[] urls) {super(urls);}static {ClassloaderTest.registerAsParallelCapable();}public static void main(String[] args) {Process1 pro = new Process1();}}

         (1)模拟ClassNotFoundException:

将本地工程bin路径下的Process1.class文件删除,在运行Process1.java就会出现,这个简单。

         (2)模拟NoSuchMethodException:

这里需要在ClassloaderTest的main函数中用到反射:

public static void main(String[] args) {ClassloaderTest loaderTest = null;try {loaderTest = new ClassloaderTest(new URL[]{new File("E:\\process.jar").toURI().toURL()});} catch (MalformedURLException e3) {e3.printStackTrace();}try {Class<?> pro = loaderTest.loadClass("org.test.Process1");Method method = null;try {method = pro.getDeclaredMethod("getStr");} catch (NoSuchMethodException | SecurityException e) {e.printStackTrace();}Object probj = null;try {probj = pro.newInstance();} catch (InstantiationException | IllegalAccessException e1) {e1.printStackTrace();}try {String str = (String) method.invoke(probj);} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {e.printStackTrace();}}catch(ClassNotFoundException e) {e.printStackTrace();}}
这里假设Process1.java中有一个getStr函数,然后在调用pro.getDeclaredMethod("getStr");会出现NoSuchMethodException

        (3)模拟NoClassDefFoundError、ClassCastException:

        这里主要是用两个类加载器加载同一个类来说明。这里首先把ClassloaderTest的main调整为:

public static void main(String[] args) {ClassloaderTest loaderTest = null;try {loaderTest = new ClassloaderTest(new URL[]{new File("E:\\process.jar").toURI().toURL()});} catch (MalformedURLException e3) {e3.printStackTrace();}try {Class<?> pro = loaderTest.loadClass("org.test.Process1");pro.newInstance();}catch(ClassNotFoundException | InstantiationException | IllegalAccessException e) {e.printStackTrace();}}

注意这里涉及到ClassloaderTest、Process1两个类,这两个类都是需要加载的。

        ClassloaderTest类,AppClassLoder加载,其本身是一个继承于URLClassLoader的类加载器,它要去加载其他类,首先自己要被加载到jvm中,ClassloaderTest是java工程中的一个类,编译后会在本地工程的bin目录下ClassloaderTest.class文件,而bin下面的class文件是由AppClassLoder加载的,所以ClassloaderTest由AppClassLoder加载。所以看一个类由哪个类加载器加载,就看该类的class文件处于什么类加载器加载的路径。另外,ClassloaderTest虽然继承于URLClassLoader,但是它的“parent”属性是AppClassLoader(因为URLClassLoader默认的parent属性是AppClassLoader),也就是向上委派的类加载器是AppClassLoader,这是用于ClassloaderTest加载其他类的,和ClassloaderTest被加载没有什么关系。可以通过jdk源码体现关系图“、”类加载委派关系图“两个维度来了解类加载器类。

        Process1类可以由AppClassLoder或ClassloaderTest来加载。如果本地工程的bin下有Process1.class,那毫无疑问是AppClassLoder加载;如果删除本地工程的bin下Process1.class,在E:\路径下放置process.jar,那Process1会由ClassloaderTest加载。

        一般在一个类中所引用到的其他类,由被引用的类所被加载的类加载器加载。

public class A {void doTest() {B b = new B();b.test();}}
       也就是说,A如果被AppClassLoader加载,那么A所引用的类B也一般由AppClassLoader加载,这是一般情况,但最正确的是看B.class在哪个类加载器加载的路径下。

a)首先看看NoClassDefFoundError:

在ClassloaderTest的main中,用Process1 probj = (Process1)pro.newInstance();替换pro.newInstance();语句;然后编译java工程,再删除bin路径下的Process1.class;再运行ClassloaderTest,到Process1 probj = (Process1)pro.newInstance();会出现如下NoClassDefFoundError异常:

Exception in thread "main" java.lang.NoClassDefFoundError: org/test/Process1
at org.test.ClassloaderTest.main(ClassloaderTest.java:102)
Caused by: java.lang.ClassNotFoundException: org.test.Process1
at java.net.URLClassLoader$2.run(URLClassLoader.java:366)
at java.net.URLClassLoader$2.run(URLClassLoader.java:1)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
... 1 more

这里解释一下。Process1 probj = (Process1)pro.newInstance();,这条语句实际上有三个动作:1.pro.newInstance();2.加载Process1.class;3.将1中的实例赋值给2中的引用。(1)在模拟过程中,做了一个动作,就是将bin下的Process1.class删除,这样”E:\“中的Process1.class会由ClassloaderTest加载,所以1中的instance是ClassloaderTest加载的Process1所产生的;(2)从打出来的Exception Stack Trace,可以看出NoClassDefFoundError是由ClassNotFoundException所引起的,为什么会出现ClassNotFoundException,这是因为ClassloaderTest类是由AppClassLoder加载的,所以ClassloaderTest中引用的Process1也应该由AppClassLoder加载,这个前面已经讲过,而AppClassLoder在当前java工程的bin路径没有找到Process1.class,所以就出现ClassNotFoundException: org.test.Process1;(3)为什么会出现NoClassDefFoundError?这是因为第3步的赋值过程,要知道在jvm中判断是否为同一个类由两个因素决定:是否由同一个类加载器加载,是否从相同内容的.class加载。

b)ClassCastException

(1)首先编译java工程;(2)在Eclipse环境中Process1 probj = (Process1)pro.newInstance();处设置断点;(3)剪切bin路径下的Process1.class到其他盘;(4)启动ClassloaderTest调试,到设置断点处;(5)将(3)中剪切的Process1.class文件在拷贝到bin路径下;(6)继续运行完当前进程,会出现ClassNotFoundException异常:
Exception in thread "main" java.lang.ClassCastException: org.test.Process1 cannot be cast to org.test.Process1
at org.test.ClassloaderTest.main(ClassloaderTest.java:101)

NoClassDefFoundError异常模拟动作不同的是,本次模拟中,在ClassloaderTest加载"E:\"路径下的Process1.class后,会将之前(3)中剪切的Process1.class文件再拷贝到bin路径下,这样AppClassLoader就能加载到ClassloaderTest中所引用到的Process1,这个时候将ClassloaderTest加载的Process1所产生的实例赋值给AppClassLoader加载的Process1引用就会出现ClassCastException。

        这样从jdk源码角度理解”委派机制“,通过实际应用且模拟类加载相关的异常,相信对jvm的类加载会有更深入的理解。

0 0
原创粉丝点击